React is arguably one of the most popular JavaScript libraries in recent years. Its simple approach to the composition of components has changed the way we write front-end (and to an extent back-end) applications fundamentally, allowing you to declaratively write UI code using a JavaScript syntax extension called JSX. This simple principle was not only easy to pick up and understand, but it also influenced dozens of other libraries to this day.
JSX is undoubtedly a game-changer in the JavaScript world, and with TypeScript’s goal to cater to all JavaScript developers, JSX found its way into TypeScript as well. In fact, TypeScript is a full-fledged JSX compiler. If you have no need for additional bundling or extra tooling, TypeScript is all you need to get your React app going. TypeScript is also immensely popular. At the time of writing, the React typings on NPM clocked in 20 million downloads per week. The fantastic tooling with VS Code and the excellent types made TypeScript the first choice for React developers around the globe.
While TypeScript’s popularity among React developers continues unabated, there is one circumstance that makes the usage of TypeScript with React a bit difficult: TypeScript isn’t the React team’s first choice. While other JSX-based libraries are now mostly written in TypeScript and therefore provide excellent types out of the box, the React team works with their own static type checker called Flow, which is similar, but ultimately incompatible with TypeScript. This means that the React types millions of developers rely on are made subsequently by a group of community contributors, and published on Definitely Typed. While @types/react are considered to be excellent, they are still just the best effort to type a library as complex as React. This inevitably leads to gaps, and where those gaps become visible, this chapter will be your guide.
In this chapter, we look at situations where React is supposed to be easy, but TypeScript gives you a hard time throwing complex error messages at you. We are going to figure out what those messages mean, how you can work around them and what solutions help you in the long run. You will also learn about various development patterns and their benefit, and how to use TypeScript’s built-in JSX support for your benefit.
What you won’t get is a basic set-up guide for React and TypeScript. The ecosystem is so vast and rich, there are many ways that lead to Rome. Pick your framework’s documentation pages and look out for TypeScript. Also note that I assume some React experience upfront. In this chapter, we deal mostly about typing React.
While there is naturally a strong inclination towards React in this chapter, you will be able to use certain learnings and apply them to other JSX-based frameworks and libraries as well.
You write a lot of standard HTML components, but you don’t want to set all necessary properties all the time.
Create proxy components, and apply a few patterns to make them usable for your scenario.
There is rarely a web application that doesn’t use buttons. Buttons have a type property which defaults to submit. This is a sensible default for forms where you perform an action over HTTP, where you POST the contents to a server-side API. But when you just want to have interactive elements on your site, the correct type for buttons is button. This is not only an aesthetic choice, but also important for accessibility.
<buttontype="button">Click me!</button>
When you write React, chances are you rarely submit a form to a server with a submit type, but interact with lots of button-type buttons. A good way to deal with situations like this is to write proxy components. They mimic HTML eleents, but preset already a couple of properties.
functionButton(props){return<buttontype="button"{...props}/>;}
The idea is that Button takes the same properties as the HTML button, and the attributes are spread out to the HTML element. Spreading attributes to HTML elements is a nice feature where you can make sure that you are able to set all the HTML properties that an element has without knowing upfront which you want to set. But how do we type them?
All HTML elements that can be used in JSX are defined through intrinsic elements in the JSX namespace. When you load React, the JSX namespace appears as a global namespace in your file, and you can access all elements via index access. So the correct prop types for Button are defined in JSX.IntrinsicElements.
An alternative to JSX.IntrinsicElements is React.ElementType, a generic type within the React package, which also includes class and function components. For proxy components, JSX.IntrinsicElements is sufficient enough, and comes with an extra benefit: Your components stay compatible to other React-like frameworks like Preact.
JSX.IntrinsicElements is a type within the global JSX namespace. Once this namespace is in scope, TypeScript is able to pick up basic elements which are compatible with your JSX-based framework.
typeButtonProps=JSX.IntrinsicElements["button"];functionButton(props:ButtonProps){return<buttontype="button"{...props}/>;}
This includes children, we spread them along! As you see, we set a button’s type to button. Since props are just JavaScript objects, it’s possible to override type by setting it as an attribute in props. If two keys with the same name are defined, the last one wins. This might be desired behaviour, but you alternatively may want to prevent you and your colleagues from overriding type. With the Omit<T, K> helper type, you can take all properties from a JSX button, but drop keys you don’t want to override.
typeButtonProps=Omit<JSX.IntrinsicElements["button"],"type">;functionButton(props:ButtonProps){return<buttontype="button"{...props}/>;}constaButton=<Buttontype="button">Hi</Button>;// ^// Type '{ children: string; type: string; }' is not// assignable to type 'IntrinsicAttributes & ButtonProps'.// Property 'type' does not exist on type// 'IntrinsicAttributes & ButtonProps'.(2322)
If you need type to be submit, you can create another proxy component.
typeSubmitButtonProps=Omit<JSX.IntrinsicElements["button"],"type">;functionSubmitButton(props:SubmitButtonProps){return<buttontype="submit"{...props}/>;}
You can extend this idea of omitting properties if you want to preset even more properties. Maybe you adhere to a design system and don’t want class names to be set arbitrarily.
typeStyledButton=Omit<JSX.IntrinsicElements["button"],"type"|"className"|"style">&{type:"primary"|"secondary";};functionStyledButton({type,...allProps}:StyledButton){return<Buttontype="button"className={`btn-${type}`}{...allProps}/>;}
This even allows you to reuse the type property name.
We dropped some props from the type definition and preset them to sensible defaults. Now we want to make sure our users don’t forget to set some props. Like the alt attribute of an image or the src attribute.
For that, we create a MakeRequired helper type that removes the optional flag.
typeMakeRequired<T,KextendskeyofT>=Omit<T,K>&Required<Pick<T,K>;
And build our own props:
typeImgProps=MakeRequired<JSX.IntrinsicElements["img"],"alt"|"src">;exportfunctionImg(props:ImgProps){return<img{...props}/>;}constanImage=<Img/>;// ^// Type '{}' is missing the following properties from type// 'Required<Pick<DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>,// HTMLImageElement>, "alt" | "src">>': alt, src (2739)
With just a few changes to the original intrinsic element’s type and a proxy component, we can make sure that our code becomes more robust, more accessible, and less error prone.
Form elements like inputs add another complexity as we need to decide where to manage state: In the browser, or in React.
Write a proxy component that uses discriminated unions and the optional never technique to ensure you won’t switch from uncontrolled to controlled at runtime.
React differentiates form elements between controlled components and uncontrolled components. When you use regular form elements like input, textarea, or select, you need to keep in mind that the underlying HTML elements control their own state. Whereas in React, the state of an element is also defined through React.
If you set the value attribute, React assumes that the element’s value is also controlled by React’s state management, which means that you are not able to modifiy this value unless you maintain the element’s state using useState and the associated setter function.
There are two ways to deal with this. First, you can choose defaultValue as a property instead of value. This will only set the value of the input in the first rendering, and subsequently leaves everything in the hands of the browser.
functionInput({value="",...allProps}:Props){return(<inputdefaultValue={value}{...allProps}/>);}
Or you manage value interally via React’s state management. Usually, it’s enough to just intersect the original input element’s props with our own type. We drop value from the intrinsic elements, and add it as an required string.
typeControlledProps=Omit<JSX.IntrinsicElements["input"],"value">&{value:string;};
Then, we wrap the input element in a proxy component. It is not best practice to keep state internally in a proxy component, but rather manage it from the outside with useState. We also forward the onChange handler we pass from the original input props.
functionInput({value="",onChange,...allProps}:ControlledProps){return(<inputvalue={value}{...allProps}onChange={onChange}/>);}functionAComponentUsingInput(){const[val,setVal]=useState("");return<Inputvalue={val}onChange={(e)=>{setVal(e.target.value);}}/>}
Furthermore, there is an interesting warning react raises when dealing with a switch from uncontrolled to controlled at runtime: Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
We can prevent this warning by making sure at compile time that we either always provide a defined string value, or that we provide a defaultValue instead. But not both. This can be solved by using a discriminated union type using the optional never technique (as seen in Recipe 3.8), and use the OnlyRequired helper type from Recipe 8.1 to derive possible properties from JSX.IntrinsicElements["input"].
importReact,{useState}from"react";// A helper type setting a few properties to be requiredtypeOnlyRequired<T,KextendskeyofT=keyofT>=Required<Pick<T,K>>&Partial<Omit<T,K>>;// Branch 1: Make "value" and "onChange" required, drop `defaultValue`typeControlledProps=OnlyRequired<JSX.IntrinsicElements["input"],"value"|"onChange">&{defaultValue?:never;};// Branch 2: Drop `value` and `onChange`, make `defaultValue` requiredtypeUncontrolledProps=Omit<JSX.IntrinsicElements["input"],"value"|"onChange">&{defaultValue:string;value?:never;onChange?:never;};typeInputProps=ControlledProps|UncontrolledProps;functionInput({...allProps}:InputProps){return<input{...allProps}/>;}functionControlled(){const[val,setVal]=useState("");return<Inputvalue={val}onChange={(e)=>setVal(e.target.value)}/>;}functionUncontrolled(){return<InputdefaultValue="Hello"/>;}
All other cases, having an optional value or having a defaultValue and trying to control values, will be prohibited by the type system.
You want to define custom hooks and get proper types.
Use tuple types or const context.
Let’s create a custom hook in React and stick to the naming convention as regular React hooks do: Returning an array (or tuple) that can be destructured. For example useState:
const[state,setState]=useState(0);
Why do we even use arrays? Because the array’s fields have no name, and you can set names on your own:
const[count,setCount]=useState(0);const[darkMode,setDarkMode]=useState(true);
So naturally, if you have a similar pattern, you also want to return an array. A custom toggle hook might look like this:
exportconstuseToggle=(initialValue:boolean)=>{const[value,setValue]=useState(initialValue);consttoggleValue=()=>setValue(!value);return[value,toggleValue];}
Nothing out of the ordinary. The only types we have to set are the types of the input parameters. Let’s try to use it:
exportconstBody=()=>{const[isVisible,toggleVisible]=useToggle(false)return(<><buttononClick={toggleVisible}>Hello</button>{/* Error. See below */}{isVisible&&<div>World</div>}</>)}// Error: Type 'boolean | (() => void)' is not assignable to// type 'MouseEventHandler<HTMLButtonElement> | undefined'.// Type 'boolean' is not assignable to type// 'MouseEventHandler<HTMLButtonElement>'.(2322)
So why does this fail? The error message might be very cryptic, but what we should look out for is the first type, which is declared incompatible: boolean | (() => void)'. This comes from returning an array. An array is a list of any length, that can hold as many elements as virtually possible. From the return value in useToggle, TypeScript infers an array type. Since the type of value is boolean (great!) and the type of toggleValue is (() => void) (a function expected to return nothing), TypeScript tells us that both types are possible in this array.
And this is what breaks the compatibility with onClick. onClick expects a function. Good, toggleValue (or toggleVisible) is a function. But according to TypeScript, it can also be a boolean! TypeScript tells you to be explicit, or at least do type checks.
But we shouldn’t need to do extra type-checks. Our code is very clear. It’s the types that are wrong. Because we’re not dealing with an array. Let’s go for a different name: Tuple. While an array is a list of values that can be of any length, we know exactly how many values we get in a tuple. Usually, we also know the type of each element in a tuple.
So we shouldn’t return an array, but a tuple at useToggle. The problem: In JavaScript an array and a tuple are indistinguishable. In TypeScript’s type system, we can distinguish them.
First possibility: Let’s be intentional with our return type. Since TypeScript — correctly! — infers an array, we have to tell TypeScript that we are expecting a tuple.
// add a return type hereexportconstuseToggle=(initialValue:boolean):[boolean,()=>void]=>{const[value,setValue]=useState(initialValue);consttoggleValue=()=>setValue(!value);return[value,toggleValue];};
With [boolean, () => void] as a return type, TypeScript checks that we are returning a tuple in this function. TypeScript does not infer anymore, but rather makes sure that your intended return type is matched by the actual values. And voila, your code doesn’t throw errors anymore.
The second option involves const context. With a tuple, we know how many elements we are expecting, and know the type of these elements. This sounds like a job for freezing the type with a const assertion.
exportconstuseToggle=(initialValue:boolean)=>{const[value,setValue]=useState(initialValue);consttoggleValue=()=>setValue(!value);// here, we freeze the array to a tuplereturn[value,toggleValue]asconst;}
The return type is now readonly [boolean, () => void], because as const makes sure that your values are constant, and not changeable. This type is a little bit different semantically, but in reality, you wouldn’t be able to change the values you return outside of useToggle. So being readonly would be slightly more correct.
You use forwardRef for your components, but you need them to be generic.
There are several solutions to this problem. Let’s discuss!
If you are creating component libraries and design systems in React, you might already have fowarded Refs to the DOM elements inside your components.
This is especially useful if you wrap basic components or leaves_ in proxy components (see Recipe 10.1, but want to use the ref property just like you’re used to:
constButton=React.forwardRef((props,ref)=>(<buttontype="button"{...props}ref={ref}>{props.children}</button>));// Usage: You can use your proxy just like you use// a regular button!constreference=React.createRef();<ButtonclassName="primary"ref={reference}>Hello</Button>
Providing types for React.forwardRef is usually pretty straightforward. The types shipped by @types/react have generic type variables that you can set upon calling React.forwardRef. In that case, explicitly annotating your types is the way to go!
typeButtonProps=JSX.IntrinsicElements["button"];constButton=React.forwardRef<HTMLButtonElement,ButtonProps>((props,ref)=>(<buttontype="button"{...props}ref={ref}>{props.children}</button>));// Usageconstreference=React.createRef<HTMLButtonElement>();<ButtonclassName="primary"ref={reference}>Hello</Button>
So far, so good. But things get a bit hairy if you have a component that accepts generic properties. The following component that produces a list of list items, where you can select each row with a button element:
typeClickableListProps<T>={items:T[];onSelect:(item:T)=>void;};functionClickableList<T>(props:ClickableListProps<T>){return(<ul>{props.items.map((item,idx)=>(<li><buttonkey={idx}onClick={()=>props.onSelect(item)}>Choose</button>{item}</li>))}</ul>);}// Usageconstitems=[1,2,3,4];<ClickableListitems={items}onSelect={(item)=>{// item is of type numberconsole.log(item);}}/>
You want the extra type-safety so you can work with a type-safe item in your onSelect callback. Say you want to create a ref to the inner ul element, how do you proceed? Let’s change the ClickableList component to an inner function component that takes a ForwardRef, and use it as an argument in the React.forwardRef function.
// The original component extended with a `ref`functionClickableListInner<T>(props:ClickableListProps<T>,ref:React.ForwardedRef<HTMLUListElement>){return(<ulref={ref}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}// As an argument in `React.forwardRef`constClickableList=React.forwardRef(ClickableListInner)
This compiles, but has one downside: We can’t assign a generic type variable for ClickableListProps. It becomes unknown by default. Which is good compared to any, but also slightly annoying. When we use ClickableList, we know which items to pass along! We want to have them typed accordingly! So how can we achieve this? The answer is tricky… and you have a couple of options.
The first option is to do a type assertion that restores the original function signature.
constClickableList=React.forwardRef(ClickableListInner)as<T>(props:ClickableListProps<T>&{ref?:React.ForwardedRef<HTMLUListElement>})=>ReturnType<typeofClickableListInner>;
Type assertions work great if you happen to have just a few situations where you need to have generic forwardRef components, but might be too clumsy when you work with lots of them. Also, you introduce an unsafe operator for something that should be default behaviour.
The second option is to create custom references with warpper components. While ref is a reserved word for React components, you can use your own, custom props to mimic a similar behavior. This works just as well.
typeClickableListProps<T>={items:T[];onSelect:(item:T)=>void;mRef?:React.Ref<HTMLUListElement>|null;};exportfunctionClickableList<T>(props:ClickableListProps<T>){return(<ulref={props.mRef}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}
You introduce a new API, though. For the record, there is also the possibility of using a wrapper component, that allows you to use forwardRef inside in an inner component and expose a custom ref property to the outside.
functionClickableListInner<T>(props:ClickableListProps<T>,ref:React.ForwardedRef<HTMLUListElement>){return(<ulref={ref}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}constClickableListWithRef=forwardRef(ClickableListInner);typeClickableListWithRefProps<T>=ClickableListProps<T>&{mRef?:React.Ref<HTMLUListElement>;};exportfunctionClickableList<T>({mRef,...props}:ClickableListWithRefProps<T>){return<ClickableListWithRefref={mRef}{...props}/>;}
Both are valid solutions if the only thing you want to achieve is passing that ref. If you want to have a consistent API, you might look for something else.
The last option is to augment forwardRef with your own type definitions. TypeScript has a feature called higher-order function type inference, that allows propagating free type parameters on to the outer function.
This sounds a lot like what we want to have with forwardRef to begin with, but for some reason it doesn’t work with our current typings. The reason is that higher-order function type inference only works on plain function types. the function declarations inside forwardRef also add properties for defaultProps, etc. Relics from the class component days. Things you might not want to use anyway.
So without the additional properties, it should be possible to use higher-order function type inference!
We are using TypeScript, we have the possibility to redeclare and redefine global module, namespace and interface declarations on our own. Declaration merging is a powerful tool, and we’re going to make use of it.
// Redecalare forwardRefdeclaremodule"react"{functionforwardRef<T,P={}>(render:(props:P,ref:React.Ref<T>)=>React.ReactElement|null):(props:P&React.RefAttributes<T>)=>React.ReactElement|null;}// Just write your components like you're used to!typeClickableListProps<T>={items:T[];onSelect:(item:T)=>void;};functionClickableListInner<T>(props:ClickableListProps<T>,ref:React.ForwardedRef<HTMLUListElement>){return(<ulref={ref}>{props.items.map((item,i)=>(<likey={i}><buttononClick={(el)=>props.onSelect(item)}>Select</button>{item}</li>))}</ul>);}exportconstClickableList=React.forwardRef(ClickableListInner);
The nice thing about this solution is that you write regular JavaScript again, and work exclusively on a type level. Also, redeclarations are module-scoped. No interference with any forwardRef calls from other modules!
You want to use the context API for globals in your app, but you don’t know how to best deal with type definitions.
Either set default properties for context and let the type be inferred, or create a partial of your context’s properties and instantiate the generic type parameter explicitly. If you don’t want to provide default values, but want to make sure that all properties are provided, create a helper function.
React’s context API allows you to share data on a global level. To use it, you need two things:
A provider. Providers pass data to a subtree.
Consumers. Consumers are components that consume the passed data inside render props
With React’s typings, you can use context without doing anything else most of the time. Everything is done using type inference and generics.
Let’s have a look! First, we create a context. Here, we want to store global application settings, like a theme, the app’s language along with the global state. When creating a React context, we want to pass default properties.
importReactfrom"react";constAppContext=React.createContext({authenticated:true,lang:"en",theme:"dark",});
And with that, everything you need to do in terms of types is done for you. We have three properties called
authenticated, lang, and theme, they are of types boolean and string respectively. React’s typings
take this information to provide you with the correct types when you use them.
Next, a component high up in your component tree needs to provide context, for example, the application’s root component. This provider trickles down the values you’ve set to every consumer below.
functionApp(){return(<AppContext.Providervalue={{authenticated:true,lang:"de",theme:"light",}}><Header/></AppContext.Provider>);}
Now, every component inside this tree can consume this context. You already get type errors when you forget a property or use the wrong type:
functionApp(){// Property 'theme' is missing in type '{ lang: string; }' but required// in type '{ lang: string; theme: string; authenticated: boolean }'.(2741)return(<AppContext.Providervalue={{lang:"de",}}><Header/></AppContext.Provider>);}
Now, let’s consume our global state. Consuming context can be done via render props. You can destructure your render props as deep as you like, to get only the props you want to deal with:
functionHeader(){return(<AppContext.Consumer>{({authenticated})=>{if(authenticated){return<h1>Loggedin!</h1>;}return<h1>Youneedtosignin</h1>;}}</AppContext.Consumer>);}
Another way of using context is via the respective useContext hook.
functionHeader(){const{authenticated}=useContext(AppContext);if(authenticated){return<h1>Loggedin!</h1>;}return<h1>Youneedtosignin</h1>;}
Because we defined our properties earlier with the right types, authenticated is of type boolean at this point. Again
we didn’t have to do anything to get this extra type safety.
The whole example above works best if we have default properties and values. Sometimes, you don’t have default values or you need to be more flexible in which properties you want to set.
Instead of inferring everything from default values, we annotate the generic type parameter explicitly, but not with the full type, but with a Partial.
We create a type for the context’s props.
typeContextProps={authenticated:boolean;lang:string;theme:string;};
And initialize the new context.
constAppContext=React.createContext<Partial<ContextProps>>({});
Changing the semantics of the context’s default properties has some side effects on your components as well. Now you don’t need to provide every value, an empty context object can just do the same! All your properties are optional.
functionApp(){return(<AppContext.Providervalue={{authenticated:true,}}><Header/></AppContext.Provider>);}
This also means that you need to check for every property if it’s defined. This doesn’t change the code where you rely on boolean values, but every other property needs to have another undefined check.
functionHeader(){const{authenticated,lang}=useContext(AppContext);if(authenticated&&lang){return<><h1>Loggedin!</h1><p>Yourlanguagesettingissetto{lang}</p></>;}return<h1>Youneedtosignin(ordon'tyouhavealanguagesetting?)</h1>;}
If you can’t provide default values and want to make sure that all properties are provided by a context provider, we can help ourselves with a little helper function. Here, we want explicit generic instantiation to supply a type, but give the right type guards so that when consuming context, all possibly undefined values are correctly set.
functioncreateContext<Propsextends{}>(){// (1)constctx=React.createContext<Props|undefined>(undefined);// (2)functionuseInnerCtx(){// (3)constc=useContext(ctx);if(c===undefined)// (4)thrownewError("Context must be consumed within a Provider");returnc;// (5)}return[useInnerCtx,ctx.ProviderasReact.Provider<Props>]asconst;// (6)}
What’s going on in createContext?
We create a function that has no function arguments, but generic type parameters. Without the connection to function parameters, we can’t instantiate Props via inference. This means that for createContext to provide proper types, we need to explicitly instantiate it.
We create a context that allows for Props or undefined. With undefined added to the type, we can pass undefined as value. No default values!
Inside createContext, we create a custom hook. This hook wraps useContext using the newly created context ctx.
Then we do a type guard where we check if the returned Props include undefined. Remember, when calling createContext, we instantiate the generic type parameter with Props | undefined. This line removes undefined from the union type again.
Which means that here, c is Props.
We assert that ctx.Provider doesn’t take undefined values. We call as const to return [useInnerContext, ctx.Provider] as a tuple type.
Use createContext similar to React.createContext.
const[useAppContext,AppContextProvider]=createContext<ContextProps>();
When using AppContextProvider, we need to provide all values.
functionApp(){return(<AppContextProvidervalue={{lang:"en",theme:"dark",authenticated:true}}><Header/></AppContextProvider>);}functionHeader(){// consuming Context doesn't change muchconst{authenticated}=useAppContext();if(authenticated){return<h1>Loggedin!</h1>;}return<h1>Youneedtosignin</h1>;}
Depending on your use case you have exact types without too much overhead for you.
You are writing higher-order components to preset certain properties for other components, but don’t know how to type them.
Use the React.ComponentType<P> type from @types/react to define a component that extends upon your preset attributes.
React is influenced by functional programming, which we see in the way components are designed (via functions), assembled (via composition), and updated (stateless, uni-directional data flow). It didn’t take long for functional programming techniques and paradigms to find their way into React development. One such technique is higher-order components, which draw inspiration from higher-order functions.
Higher-order functions accept one or more parameters to return a new function. Sometimes those parameters are here to prefill certain other parameters, as we see in e.g. all currying recipes from Chapter 7. Higher order components are similar: They take one or more components and return themselves another component. Usually, you create them to prefill certain properties where you want to make sure that they won’t be changed later on.
Let’s think about a general-purpose Card component, which takes title and content as strings.
typeCardProps={title:string;content:string;};functionCard({title,content}:CardProps){return(<><h2>{title}</h2><div>{content}</div></>);}
You use this card to present certain events, like warnings, information bubbles, and error messages. The most basic information card has "Info" as its title.
<Cardtitle="Info"content="Your task has been processed"/>;
You could subset the properties of Card to only allow for a certain subset of strings for title, but on the other hand, you want to be able to reuse Card as much as possible. You rather create a new component that already sets title to "Info", and only allows for other properties to be set.
constInfo=withInjectedProps({title:"Info"},Card);// This should work<Infocontent="Your task has been processed"/>;// This should throw an error<Infocontent="Your task has been processed"title="Warning"/>;
In other words, you inject a subset of properties already, and only set the remaining ones with the newly created component. A function withInjectedProps is easily written.
functionwithInjectedProps(injected,Component){returnfunction(props){constnewProps={...injected,...props};return<Component{...newProps}/>;};}
It takes the injected props and a Component as parameters, returns a new function component that takes the remaining props as parameters, and instantiates the original component with merged properties.
So how do we type withInjectedProps? Let’s look at the result, and see what’s inside.
functionwithInjectedProps<Textends{},UextendsT>(// (1)injected:T,Component:React.ComponentType<U>// (2)){returnfunction(props:Omit<U,keyofT>){// (3)constnewProps={...injected,...props}asU;// (4)return<Component{...newProps}/>;};}
This is what’s going on:
We need to define two generic type parameters. T for the props we already inject, it extends from {} to make sure we only pass objects. U is a generic type parameter for all props of Component. U extends T, which means that U is a subset of T. This says that U has more properties than T, but needs to include what T already has defined.
We define Component to be of type React.ComponentType<U>. This includes class components as well as function components, and says that props will be set to U. With the relationship of T and U and the way we defined the parameters of withInjectedProps, we make sure that everything that will be passed for Component defines a subset of properties for Component with injected. If we have a typo, we already get the first error message!
The function component that will be returned takes the remaining props. With Omit<U, keyof T> we make sure that we don’t allow prefilled attributes to be set again.
Merging T and Omit<U, keyof T> should result in U again, but since generic type parameters can be explicitly instantiated with something different, they might not fit Component again. A little type assertion helps us to make sure that the props are actually what we want.
And that’s it! With those new types, we get proper auto-complete and errors:
constInfo=withInjectedProps({title:"Info"},Card);<Infocontent="Your task has been processed"/>;<Infocontent="Your task has been processed"title="Warning"/>;// ^// Type '{ content: string; title: string; }' is not assignable// to type 'IntrinsicAttributes & Omit<CardProps, "title">'.// Property 'title' does not exist on type// 'IntrinsicAttributes & Omit<CardProps, "title">'.(2322)
withInjectedProps is so flexible, that we can derive higher order functions that create higher order components for various situations, like withTitle, which is just here to prefill title attributes of type string.
functionwithTitle<Uextends{title:string}>(title:string,Component:React.ComponentType<U>){returnwithInjectedProps({title},Component);}
Your functional programming goodness knows no boundaries.
You want to get the best possible typings for all browser events in React, and use the type system to restrict your callbacks to compatible elements.
Use the event types of @types/react and specialize on components using generic type parameters.
Web applications live by interacting with them. Events are key, and TypeScript’s React typings have great support for events, but require you not to use the native events from lib.dom.d.ts. If you do, React throws errors at you:
typeWithChildren<T={}>=T&{children?:React.ReactNode};typeButtonProps={onClick:(event:MouseEvent)=>void;}&WithChildren;functionButton({onClick,children}:ButtonProps){return<buttononClick={onClick}>{children}</button>;// ^// Type '(event: MouseEvent) => void' is not assignable to// type 'MouseEventHandler<HTMLButtonElement>'.// Types of parameters 'event' and 'event' are incompatible.// Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is missing the following// properties from type 'MouseEvent': offsetX, offsetY, x, y,// and 14 more.(2322)}
React uses its own event system, which we refer to as synthetic events. Synthetic events are cross-browser wrappers around the browser’s native event, with the same interface as its native counterpart, but aligned for compatibility. A change to the type from @types/react makes your callbacks compatible again.
importReactfrom"react";typeWithChildren<T={}>=T&{children?:React.ReactNode};typeButtonProps={onClick:(event:React.MouseEvent)=>void;}&WithChildren;functionButton({onClick,children}:ButtonProps){return<buttononClick={onClick}>{children}</button>;}
The browser’s MouseEvent and React.MouseEvent are different enough for TypeScript’s structural type system, meaning that there are some missing properties in the synthetic counterparts. You can see in the error message above that the original MouseEvent has 18 properties more than React.MouseEvent, some of them arguably important, like coordinates and offsets, which come in really hand if you e.g. want to draw on a canvas.
If you want to access properties from the original event, you can use the nativeEvent property.
functionhandleClick(event:React.MouseEvent){console.log(event.nativeEvent.offsetX,event.nativeEvent.offsetY);}constbtn=<ButtononClick={handleClick}>Hello</Button>;
Events supported are: AnimationEvent, ChangeEvent, ClipboardEvent, CompositionEvent, DragEvent, FocusEvent, FormEvent, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, TransitionEvent, WheelEvent. As well as SyntheticEvent, for all other events.
So far, we just apply the correct types to make sure we don’t have any compiler errors. Easy enough. But we’re not only using TypeScript to fulfill the ceremony of applying types for the compiler to shut up, but also to prevent ourselves from situations that might be problematic.
Let’s think about a button again. Or a link (the a element). Those elements are supposed to be clicked, that’s their purpose, but in the browser, click events can be received by every element. Nothing keeps you from adding onClick to a div element, the element that has the least semantic meaning of all elements, and no assistive technology will tell you that a div can receive a MouseEvent unless you add lots and lots of additional attributes to it.
Wouldn’t it be great if we can prevent our colleagues and ourselves to use the defined event handlers on the wrong elements? React.MouseEvent is a generic type that takes compatible elements as its first type. This is set to Element, which is the base type for all elements in the browser. But you are able to define a smaller set of compatible elements by sub-typing this generic parameter.
typeWithChildren<T={}>=T&{children?:React.ReactNode};// Button maps to an HTMLButtonElementtypeButtonProps={onClick:(event:React.MouseEvent<HTMLButtonElement>)=>void;}&WithChildren;functionButton({onClick,children}:ButtonProps){return<buttononClick={onClick}>{children}</button>;}// handleClick accepts events from HTMLButtonElement or HTMLAnchorElementfunctionhandleClick(event:React.MouseEvent<HTMLButtonElement|HTMLAnchorElement>){console.log(event.currentTarget.tagName);}letbutton=<ButtononClick={handleClick}>Works</Button>;letlink=<ahref="/"onClick={handleClick}>Works</a>;letbroken=<divonClick={handleClick}>Doesnotwork</div>;// ^// Type '(event: MouseEvent<HTMLButtonElement | HTMLAnchorElement,// MouseEvent>) => void' is not assignable to type//'MouseEventHandler<HTMLDivElement>'.// Types of parameters 'event' and 'event' are incompatible.// Type 'MouseEvent<HTMLDivElement, MouseEvent>' is not assignable to// type 'MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>'.// Type 'HTMLDivElement' is not assignable to type #// 'HTMLButtonElement | HTMLAnchorElement'.
Where React’s types give you more flexibility in some areas, it lacks features in others. For example, the browser native InputEvent is not supported in @types/react. The synthetic event system is meant as a cross-browser solution, and some of React’s compatible browsers still lack implementation of InputEvent. Until they catch up, it’s safe for you to use the base event SyntheticEvent.
functiononInput(event:React.SyntheticEvent){event.preventDefault();// do something}constinp=<inputtype="text"onInput={onInput}/>;
Now you get at least some type safety.
You create a proxy component (see Recipe 10.1) that needs to behave as one of many different HTML elements. It’s hard to get the right typings.
Assert forwarded properties as any or use the JSX factory React.createElement directly.
A common pattern in React is to define polymorphic (or as) components, which pre-define behavior, but can act as different elements. Think of a call-to-action button, or CTA, which can either be a link to a website, or an actual HTML button. If you want to style them similarly, they should behave alike, but depending on the context they should have the right HTML element for the right action.
Selecting the right element is an important accessibility factor. a and button elements represent something users can click, but the semantics of a are fundamentally different from the semantics of a button. a is short for anchor, and needs to have a reference (href) to a destination. A button can be clicked, but the action is usually scripted via JavaScript. Both elements can look the same, but act differently. But not only do they act differently, but they also are announced differently using assistive technologies, like screen readers. Think about your users and select the right element for the right purpose.
The idea is that you have an as prop in your component that selects the element type. Depending on the element type of as, you can forward properties that fit the element type. Of course, you can combine this pattern with everything you’ve seen in Recipe 10.1.
<Ctaas="a"href="https://typescript-cookbook.com">Heyhey</Cta><Ctaas="button"type="button"onClick={(e)=>{/* do something */}}>Mymy</Cta>
When throwing TypeScript into the mix, you want to make sure that you get autocomplete for the right props, and errors for the wrong properties. If you add an href to a button, TypeScript should give you the correct squiggly lines.
// Type '{ children: string; as: "button"; type: "button"; href: string; }'// is not assignable to type 'IntrinsicAttributes & { as: "button"; } &// ClassAttributes<HTMLButtonElement> &// ButtonHTMLAttributes<HTMLButtonElement> & { ...; }'.// Property 'href' does not exist on type ... (2322)// v<Ctaas="button"type="button"href=""ref={(el)=>el?.id}>Mymy</Cta>
Let’s try to type Cta. First, we develop the component without types at all. In JavaScript, things don’t look too complicated.
functionCta({as:Component,...props}){return<Component{...props}/>;}
We extract the as prop and rename it as Component. This is a destructuring mechanism from JavaScript that is syntactically similar to a TypeScript annotation but works on destructured properties, and not on the object itself (where you’d need a type annotation). So don’t be irritated. We rename it to an uppercase component so we can instantiate it via JSX. The remaining props will be collected in ...props, and spread out when creating the component. Note that you can also spread out children with ...props, a nice little side effect of JSX.
When we want to type Cta, we create a CtaProps type that works on either "a" elements or "button" elements and takes the remaining props from JSX.IntrinsicElements. Very similar to what we’ve seen in Recipe 10.1.
typeCtaElements="a"|"button";typeCtaProps<TextendsCtaElements>={as:T;}&JSX.IntrinsicElements[T];
When we wire up our types to Cta, we see that the function signature works very well with just a few extra annotations. But when instantiating the component, we get quite an elaborate error that tells us how much is going wrong!
functionCta<TextendsCtaElements>({as:Component,...props}:CtaProps<T>){return<Component{...props}/>;// ^// Type 'Omit<CtaProps<T>, "as" | "children"> & { children: ReactNode; }'// is not assignable to type 'IntrinsicAttributes &// LibraryManagedAttributes<T, ClassAttributes<HTMLAnchorElement> &// AnchorHTMLAttributes<HTMLAnchorElement> & ClassAttributes<...> &// ButtonHTMLAttributes<...>>'.// Type 'Omit<CtaProps<T>, "as" | "children"> & { children: ReactNode; }' is not// assignable to type 'LibraryManagedAttributes<T, ClassAttributes<HTMLAnchorElement>// & AnchorHTMLAttributes<HTMLAnchorElement> & ClassAttributes<...>// & ButtonHTMLAttributes<...>>'.(2322)}
So where does this message come from? For TypeScript to work correctly with JSX, we need to resort to type definitions in a global namespace called JSX. If this namespace is in scope, TypeScript knows which elements that aren’t components can be instantiated, and which attributes they can accept. These are the JSX.IntrinsicElements we use in this example and in Recipe 10.1.
One type that also needs to be defined is LibraryManagedAttributes. This type is used to provide attributes that are either defined by the framework itself (like key), or via means like defaultProps.
exportinterfaceProps{name:string;}functionGreet({name}:Props){return<div>Hello{name.toUpperCase()}!</div>;}// Goes into LibraryManagedAttributesGreet.defaultProps={name:"world"};// Type-checks! No type assertions needed!letel=<Greetkey={1}/>;
React’s typings solve LibraryManagedAttributes by using a conditional type. And as we see in Recipe 12.7, conditional types won’t be expanded with all possible variants of a union type when being evaluated. This means that TypeScript won’t be able to check that your typings fit the components because it won’t be able to evaluate LibraryManagedAttributes.
One workaround for this is to assert props to any.
functionCta<TextendsCtaElements>({as:Component,...props}:CtaProps<T>){return<Component{...(propsasany)}/>;}
Which works, but is a sign of an unsafe operation which, well, shouldn’t be unsafe. Another way is to not use JSX in this case, but rather use the JSX factory React.createElement.
Every JSX call is syntactic sugar to a JSX factory call.
<h1className="headline">HelloWorld</h1>// will be transformed toReact.createElement("h1",{className:"headline"},["Hello World"]);
If you use nested components, the third parameter of createElement will contain nested factory function calls. React.createElement is much easier to call than JSX, and TypeScript won’t resort to the global JSX namespace when creating new elements. Sounds like a perfect workaround for our needs.
React.createElement needs three arguments: The component, the props, and the children. Right now, we’ve smuggled all child components with props, but for React.createElement we need to be explicit. This also means that we need to explicitly define children.
For that, we create a WithChildren<T> helper type. It takes an existing type and adds optional children in form of React.ReactNode.
typeWithChildren<T={}>=T&{children?:React.ReactNode};
WithChildren is highly flexible. We can either wrap the type of our props with it.
typeCtaProps<TextendsCtaElements>=WithChildren<{as:T;}&JSX.IntrinsicElements[T]>;
Or we create a union.
typeCtaProps<TextendsCtaElements>={as:T;}&JSX.IntrinsicElements[T]&WithChildren;
Since T is set to {} by default, the type becomes universally usable. This makes it a lot easier for you to attach children whenever you need them. As a next step, we destructure children out of props, and pass all arguments into React.createElement.
functionCta<TextendsCtaElements>({as:Component,children,...props}:CtaProps<T>){returnReact.createElement(Component,props,children);}
And with that, your polymorphic component accepts the right parameters without any errors.