Looking at the source for ComponentProps shows that this is a clever wrapper for JSX.IntrinsicElements, whereas the second method relies on specialized interfaces with unfamiliar naming/capitalization.
Note: There are over 50 of these specialized interfaces available - look for HTMLAttributes in our @types/react commentary.
Ultimately, we picked the ComponentProps method as it involves the least TS specific jargon and has the most ease of use. But you'll be fine with either of these methods if you prefer.
Definitely not React.HTMLProps or React.HTMLAttributes#
This is what happens when you use React.HTMLProps:
Just as you can make generic functions and classes in TypeScript, you can also make generic components to take advantage of the type system for reusable type safety. Both Props and State can take advantage of the same generic types, although it probably makes more sense for Props than for State. You can then use the generic type to annotate types of any variables defined inside your function / class scope.
interfaceProps<T>{
items:T[];
renderItem:(item: T)=>React.ReactNode;
}
functionList<T>(props:Props<T>){
const{ items, renderItem }= props;
const[state, setState]=React.useState<T[]>([]);// You can use type T in List function scope.
children is usually not defined as a part of the props type. Unless children are explicitly defined as a part of the props type, an attempt to use props.children in JSX or in the function body will fail:
interfaceWrapperProps<T>{
item:T;
renderItem:(item: T)=>React.ReactNode;
}
/* Property 'children' does not exist on type 'WrapperProps<T>'. */
Type '{ children: string; item: string; renderItem: (item: string) => string; }' is not assignable to type 'IntrinsicAttributes & WrapperProps<string>'.
Property 'children' does not exist on type 'IntrinsicAttributes & WrapperProps<string>'.
Some API designs require some restriction on children passed to a parent component. It is common to want to enforce these in types, but you should be aware of limitations to this ability.
The thing you cannot do is specify which components the children are, e.g. If you want to express the fact that "React Router <Routes> can only have <Route> as children, nothing else is allowed" in TypeScript.
This is because when you write a JSX expression (const foo =
), the resultant type is blackboxed into a generic JSX.Element type. (thanks @ferdaber)
Components, and JSX in general, are analogous to functions. When a component can render differently based on their props, it's similar to how a function can be overloaded to have multiple call signatures. In the same way, you can overload a function component's call signature to list all of its different "versions".
A very common use case for this is to render something as either a button or an anchor, based on if it receives a href attribute.
<Link<RouterLinkProps> to="/">My link</Link>;// ok
<Link<AnchorProps> href="/">My link</Link>;// ok
<Link<RouterLinkProps> to="/" href="/">
My link
</Link>;// error
Approach: Composition
If you want to conditionally render a component, sometimes is better to use React's composition model to have simpler components and better to understand typings:
Here is a brief intuition for Discriminated Union Types:
typeUserTextEvent={
type:"TextEvent";
value:string;
target:HTMLInputElement;
};
typeUserMouseEvent={
type:"MouseEvent";
value:[number,number];
target:HTMLElement;
};
typeUserEvent=UserTextEvent|UserMouseEvent;
functionhandle(event: UserEvent){
if(event.type==="TextEvent"){
event.value;// string
event.target;// HTMLInputElement
return;
}
event.value;// [number, number]
event.target;// HTMLElement
}
Take care: TypeScript does not narrow the type of a Discriminated Union on the basis of typeof checks. The type guard has to be on the value of a key and not it's type.
The above example does not work as we are not checking the value of event.value but only it's type. Read more about it microsoft/TypeScript#30506 (comment)
To streamline this you may also combine this with the concept of User-Defined Type Guards:
Say you want a Text component that gets truncated if truncate prop is passed but expands to show the full text when expanded prop is passed (e.g. when the user clicks the text).
You want to allow expanded to be passed only if truncate is also passed, because there is no use for expanded if the text is not truncated.
Usage example:
constApp:React.FC=()=>(
<>
{/* these all typecheck */}
<Text>not truncated</Text>
<Texttruncate>truncated</Text>
<Texttruncateexpanded>
truncate-able but expanded
</Text>
{/* TS error: Property 'truncate' is missing in type '{ children: string; expanded: true; }' but required in type '{ truncate: true; expanded?: boolean | undefined; }'. */}
Sometimes when intersecting types, we want to define our own version of a prop. For example, I want my component to have a label, but the type I am intersecting with also has a label prop. Here's how to extract that out:
exportinterfaceProps{
label:React.ReactNode;// this will conflict with the InputElement's label
When your component defines multiple props, chances of those conflicts increase. However you can explicitly state that all your fields should be removed from the underlying component using the keyof operator:
exportinterfaceProps{
label:React.ReactNode;// conflicts with the InputElement's label
onChange:(text: string)=>void;// conflicts with InputElement's onChange
As you can see from the Omit example above, you can write significant logic in your types as well. type-zoo is a nice toolkit of operators you may wish to check out (includes Omit), as well as utility-types (especially for those migrating from Flow).
There are a lot of places where you want to reuse some slices of props because of prop drilling,
so you can either export the props type as part of the module or extract them (either way works).
The advantage of extracting the prop types is that you won't need to export everything, and a refactor of the source of truth component will propagate to all consuming components.
You can also use them to strongly type custom event handlers if they're not written at the call sites themselves
(i.e. inlined with the JSX attribute):
// my-inner-component.tsx
exportfunctionMyInnerComponent(props:{
onSomeEvent(
event: ComplexEventObj,
moreArgs: ComplexArgs
): SomeWeirdReturnType;
}){
/* ... */
}
// my-consuming-component.tsx
exportfunctionMyConsumingComponent(){
// event and moreArgs are contextually typed along with the return value
Advice: Where possible, you should try to use Hooks instead of Render Props. We include this merely for completeness.
Sometimes you will want to write a function that can take a React element or a string or something else as a prop. The best Type to use for such a situation is React.ReactNode which fits anywhere a normal, well, React Node would fit:
exportinterfaceProps{
label?:React.ReactNode;
children:React.ReactNode;
}
exportconstCard=(props: Props)=>{
return(
<div>
{props.label &&<div>{props.label}</div>}
{props.children}
</div>
);
};
If you are using a function-as-a-child render prop:
Simply throwing an exception is fine, however it would be nice to make TypeScript remind the consumer of your code to handle your exception. We can do that just by returning instead of throwing: