[React Typescript] Strongly typed React component `as`
The `as` Prop in React
Option 1:
import { Equal, Expect } from '../helpers/type-utils';
export const Wrapper = <TProps extends keyof JSX.IntrinsicElements>(
props: {
as: TProps;
} & JSX.IntrinsicElements[TProps]
) => {
const Comp = props.as as string;
return <Comp {...(props as JSX.IntrinsicElements[TProps])}></Comp>;
};
const Example1 = () => {
return (
<>
<Wrapper
as="button"
// @ts-expect-error doesNotExist is not a valid prop
doesNotExist
></Wrapper>
<Wrapper
as="button"
// e should be inferred correctly
onClick={(e) => {
type test = Expect<
Equal<typeof e, React.MouseEvent<HTMLButtonElement>>
>;
}}
></Wrapper>
</>
);
};
/**
* Should work specifying a 'div'
*/
const Example2 = () => {
return (
<>
<Wrapper
as="div"
// @ts-expect-error doesNotExist is not a valid prop
doesNotExist
></Wrapper>
<Wrapper
as="div"
// e should be inferred correctly
onClick={(e) => {
type test = Expect<Equal<typeof e, React.MouseEvent<HTMLDivElement>>>;
}}
></Wrapper>
</>
);
};
Option 2: A huge distrination union
export const Wrapper = <TAs extends keyof JSX.IntrinsicElements>(
props: {
as: TAs;
} & ComponentProps<TAs>
) => {
const Comp = props.as as string;
return <Comp {...(props as any)}></Comp>;
};
Component `as` with Custom component
import React, { ElementType } from "react";
import { Equal, Expect } from "../helpers/type-utils";
export const Wrapper = <TAs extends ElementType>(
props: {
as: TAs;
} & React.ComponentPropsWithoutRef<TAs>
) => {
const Comp = props.as as string;
return <Comp {...(props as any)}></Comp>;
};
const Custom = (props: { thisIsRequired: boolean }) => {
return <a />;
};
const Example2 = () => {
return (
<>
<Wrapper as={Custom} thisIsRequired />
<Wrapper
as={Custom}
// @ts-expect-error incorrectProp should not be allowed
incorrectProp
/>
{/* @ts-expect-error thisIsRequired is not being passed */}
<Wrapper as={Custom}></Wrapper>
</>
);
};
Utils function from React:
/**
* NOTE: prefer ComponentPropsWithRef, if the ref is forwarded,
* or ComponentPropsWithoutRef when refs are not supported.
*/
type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =
T extends JSXElementConstructor<infer P>
? P
: T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T]
: {};
type ComponentPropsWithRef<T extends ElementType> =
T extends (new (props: infer P) => Component<any, any>)
? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
: PropsWithRef<ComponentProps<T>>;
type ComponentPropsWithoutRef<T extends ElementType> =
PropsWithoutRef<ComponentProps<T>>;
Component `as` prop with default value
Solution 1: Problem for this solution is we lost the autocompletion for `as="button"
`
import { ComponentPropsWithoutRef, ElementType } from "react";
import { Equal, Expect } from "../helpers/type-utils";
export const Link = <T extends ElementType = "a">(
props: {
as?: T;
} & ComponentPropsWithoutRef<T>
) => {
const { as: Comp = "a", ...rest } = props;
return <Comp {...(rest as any)}></Comp>;
};
/**
* Should work without specifying 'as'
*/
const Example1 = () => {
return (
<>
<Link
// @ts-expect-error doesNotExist is not a valid prop
doesNotExist
></Link>
<Link
// e should be inferred correctly
onClick={(e) => {
type test = Expect<
Equal<typeof e, React.MouseEvent<HTMLAnchorElement>>
>;
}}
></Link>
</>
);
};
/**
* Should work specifying a 'button'
*/
const Example2 = () => {
return (
<>
<Link
as="button"
// @ts-expect-error doesNotExist is not a valid prop
doesNotExist
></Link>
<Link
as="button"
// e should be inferred correctly
onClick={(e) => {
type test = Expect<
Equal<typeof e, React.MouseEvent<HTMLButtonElement>>
>;
}}
></Link>
</>
);
};
/**
* Should work with Custom components!
*/
const Custom = (
props: { thisIsRequired: boolean },
ref: React.ForwardedRef<HTMLAnchorElement>
) => {
return <a ref={ref} />;
};
const Example3 = () => {
return (
<>
<Link as={Custom} thisIsRequired />
<Link
as={Custom}
// @ts-expect-error incorrectProp should not be allowed
incorrectProp
/>
{/* @ts-expect-error thisIsRequired is not being passed */}
<Link as={Custom}></Link>
</>
);
};
Solution 2: This approach won't lost autocompletion 💯
export const Link = <T extends ElementType>(
props: {
as?: T;
} & ComponentPropsWithoutRef<ElementType extends T ? "a" : T>
) => {
const { as: Comp = "a", ...rest } = props;
return <Comp {...(rest as any)}></Comp>;
};
The `as` props for `forwardRef` ✅
import { ComponentProps, ElementType, forwardRef, useRef } from "react";
import { Equal, Expect } from "../helpers/type-utils";
// Added fixedForwardRef from a previous exercise
type FixedForwardRef = <T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactNode
) => (props: P & React.RefAttributes<T>) => React.ReactNode;
const fixedForwardRef = forwardRef as FixedForwardRef;
// Added a DistributiveOmit type
type DistributiveOmit<T, TOmitted extends PropertyKey> = T extends any
? Omit<T, TOmitted>
: never;
function UnwrappedLink<T extends ElementType>(
props: {
as?: T;
} & DistributiveOmit<ComponentProps<ElementType extends T ? "a" : T>, "as">
) {
const { as: Comp = "a", ...rest } = props;
return <Comp {...rest}></Comp>;
}
const Link = fixedForwardRef(UnwrappedLink);
/**
* Should work without specifying 'as'
*/
const Example1 = () => {
const ref = useRef<HTMLAnchorElement>(null);
const wrongRef = useRef<HTMLDivElement>(null);
return (
<>
<Link ref={ref} />
<Link
// @ts-expect-error incorrect ref
ref={wrongRef}
/>
<Link
// @ts-expect-error doesNotExist is not a valid prop
doesNotExist
></Link>
<Link
// e should be inferred correctly
onClick={(e) => {
type test = Expect<
Equal<typeof e, React.MouseEvent<HTMLAnchorElement>>
>;
}}
></Link>
</>
);
};
/**
* Should work specifying a 'button'
*/
const Example2 = () => {
const ref = useRef<HTMLButtonElement>(null);
const wrongRef = useRef<HTMLSpanElement>(null);
return (
<>
{/* CHECK ME! Check if autocomplete works on 'as' */}
<Link as="button" />
<Link as="button" ref={ref} />
<Link
as="button"
// @ts-expect-error incorrect ref
ref={wrongRef}
/>
<Link
as="button"
// @ts-expect-error doesNotExist is not a valid prop
doesNotExist
></Link>
<Link
as="button"
// e should be inferred correctly
onClick={(e) => {
type test = Expect<
Equal<typeof e, React.MouseEvent<HTMLButtonElement>>
>;
}}
></Link>
</>
);
};
/**
* Should work with Custom components!
*/
const Custom = forwardRef(
(
props: { thisIsRequired: boolean },
ref: React.ForwardedRef<HTMLAnchorElement>
) => {
return <a ref={ref} />;
}
);
const Example3 = () => {
const ref = useRef<HTMLAnchorElement>(null);
const wrongRef = useRef<HTMLDivElement>(null);
return (
<>
<Link as={Custom} thisIsRequired />
<Link
as={Custom}
// @ts-expect-error incorrectProp should not be allowed
incorrectProp
/>
{/* @ts-expect-error thisIsRequired is not being passed */}
<Link as={Custom}></Link>
<Link as={Custom} ref={ref} thisIsRequired />
<Link
as={Custom}
// @ts-expect-error incorrect ref
ref={wrongRef}
thisIsRequired
/>
</>
);
};
分类:
React
, TypeScript
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2022-08-31 [Typescript Challenges] 3. Easy - Tuple to Object
2022-08-31 [Typescript Challenges] 2. Easy -- readonly
2022-08-31 [Typescript Challenges] 1. Easy - Pick
2022-08-31 [Pattern] Flyweight
2021-08-31 [Cloud Architect] 3. Business Objectives
2021-08-31 [AWS] Launch configuration vs Launch template
2020-08-31 [Prostgres] Select Grouped and Aggregated Data with SQL