Forwarding Refs: As the React docs themselves note, most usecases will not need to obtain a ref to the inner element. But for people making reusable component libraries, you will need to forwardRef the underlying element, and then you can use ComponentPropsWithRef to grab props for your wrapper component. Check our docs on forwarding Refs for more.
In future, the need to forwardRef may go away in React 17+, but for now we still have to deal with this. ๐
Why not ComponentProps or IntrinsicElements or [Element]HTMLAttributes or HTMLProps or HTMLAttributes?
You CAN use ComponentProps in place of ComponentPropsWithRef, but you may prefer to be explicit about whether or not the component's refs are forwarded, which is what we have chosen to demonstrate. The tradeoff is slightly more intimidating terminology.
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 = <MyComponent foo='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)
Discriminated Unions in TypeScript can also work with hook dependencies in React. The type matched is automatically updated when the corresponding union member based on which a hook depends, changes. Expand more to see an example usecase.
typeSingleElement={
isArray:true;
value:string[];
};
typeMultiElement={
isArray:false;
value:string;
};
typeProps=SingleElement|MultiElement;
functionSequence(p: Props){
returnReact.useMemo(
()=>(
<div>
value(s):
{p.isArray && p.value.join(",")}
{!p.isArray && p.value}
</div>
),
[p.isArray, p.value]// TypeScript automatically matches the corresponding value type based on dependency change
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).
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: