React types in TypeScript
Iâm using React, Iâm using TypeScript. Most of the time TypeScript is doing a great job in inferring the types, but sometimes when I need to add typings, I might just go mad.
Iâm going through some of the commonly used types for React, so you and I wonât panic anymore in the future.
1. ReactElement
So maybe you are already familiar with React Element, simply the JSX element like
<Component> or <p> are ReactElement.
The name comes from Element in DOM,
and in HTML, we are using the same XML tag like <p>.
We already know that in most cases, JSX are transpiled in to a function call of React.createElement(),
for <Component/> it generates something like below.
js{type: Component,props,key,}
js{type: Component,props,key,}
And thus we can understand why ReactElement is typed as this.
interface ReactElement<P = any,T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>--------------------------isn't this a bit duplicate?
> {type: T;props: P;key: Key | null;}type JSXElementConstructor<P> =| ((props: P,) => ReactElement<any, any> | null)Function components
| (new (props: P) => Component<any, any>);Class components
interface ReactElement<P = any,T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>--------------------------isn't this a bit duplicate?
> {type: T;props: P;key: Key | null;}type JSXElementConstructor<P> =| ((props: P,) => ReactElement<any, any> | null)Function components
| (new (props: P) => Component<any, any>);Class components
JSXElementConstructor basically means the components that could be used in JSX, it must return ReactElement.
Now we can see why the ReactElement is defined like this. type could be string because of intrinsic
html tags like <p>.
tsxfunctionC () {return <p >aa</p >}constelement = <C />
tsxfunctionC () {return <p >aa</p >}constelement = <C />
Oops, why JSX.Element above?
JSX.Element is just an alias for ReactElement. Because JSX itself is
born from React but it is independent from React,
projects other than React could have different
types though they share the same JSX syntax.
declare global {/*** @deprecated Use `React.JSX` instead of the global `JSX` namespace.*/namespace JSX {interface Element extends React.ReactElement<any, any> { }interface ElementClass extends React.Component<any> {render(): React.ReactNode;}}}
declare global {/*** @deprecated Use `React.JSX` instead of the global `JSX` namespace.*/namespace JSX {interface Element extends React.ReactElement<any, any> { }interface ElementClass extends React.Component<any> {render(): React.ReactNode;}}}
2. ReactNode
In DOM, Element is subtype of Node. Similarly, ReactElement is subtype of ReactNode. ReactNode contains stuff that is not in the form of JSX elements.
/*** For internal usage only.* Different release channels declare additional types of ReactNode this particular release channel accepts.* App or library types should never augment this interface.*/interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES {}type ReactNode =| ReactElement| string| number| ReactFragment| ReactPortal| boolean| null| undefined| DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES];Yep, don't use it or you will be fired by Meta!
Well, you'll have to join it first.
/*** For internal usage only.* Different release channels declare additional types of ReactNode this particular release channel accepts.* App or library types should never augment this interface.*/interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES {}type ReactNode =| ReactElement| string| number| ReactFragment| ReactPortal| boolean| null| undefined| DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES];Yep, don't use it or you will be fired by Meta!
Well, you'll have to join it first.
Just as Node has Text
and Comment,
ReactNode has primitive types such as string, number, null .etc.
Wait! Why are ReactFragment and ReactPortal not part of ReactElement? I use them quite often đ¤.
Good question, ReactFragment is not the React.Fragment we use.
tstype ReactFragment = Iterable<ReactNode>;
tstype ReactFragment = Iterable<ReactNode>;
We can see that Iterable
is a better type tha Array,
and ReactFragment basically means ReactNode list, pretty useful for children.
And for the React.Fragment we commonly use, it is ExoticComponent. It doesnât mean much, just indicating that this component is special. React handles it differently.
tsximportReact , {Fragment } from 'react'constel = <Fragment ><p /></Fragment >
tsximportReact , {Fragment } from 'react'constel = <Fragment ><p /></Fragment >
While for ReactPortal, it extends ReactElement, but with children hoisted on root level.
tsinterface ReactPortal extends ReactElement {key: Key | null;children: ReactNode;}
tsinterface ReactPortal extends ReactElement {key: Key | null;children: ReactNode;}
As mentioned in How does React Portal work internally,
createPortal() returns a special element.
export function createPortal(children: ReactNodeList,containerInfo: any,implementation: any,key: ?string = null): ReactPortal {return {// This tag allow us to uniquely identify this as a React Portal$$typeof: REACT_PORTAL_TYPE,key: key == null ? null : "" + key,children,usually
childrenresides underprops,but Portal only has no props other than children
so it is put here.
containerInfo,implementation,};}
export function createPortal(children: ReactNodeList,containerInfo: any,implementation: any,key: ?string = null): ReactPortal {return {// This tag allow us to uniquely identify this as a React Portal$$typeof: REACT_PORTAL_TYPE,key: key == null ? null : "" + key,children,usually
childrenresides underprops,but Portal only has no props other than children
so it is put here.
containerInfo,implementation,};}
3. FunctionComponent
interface FunctionComponent<P = {}> {(props: P, context?: any): ReactElement<any, any> | null;What is this context?
propTypes?: WeakValidationMap<P> | undefined;contextTypes?: ValidationMap<any> | undefined;defaultProps?: Partial<P> | undefined;displayName?: string | undefined;}
interface FunctionComponent<P = {}> {(props: P, context?: any): ReactElement<any, any> | null;What is this context?
propTypes?: WeakValidationMap<P> | undefined;contextTypes?: ValidationMap<any> | undefined;defaultProps?: Partial<P> | undefined;displayName?: string | undefined;}
Nothing fancy here. The context is actually used for forwardRef()
since it has a second argument of ref.
import React, { forwardRef } from 'react';const MyInput = forwardRef(function MyInput(props: {label: string}, ref) {const { label, ...otherProps } = props;return (<label>{label}<input {...otherProps} /></label>);});
import React, { forwardRef } from 'react';const MyInput = forwardRef(function MyInput(props: {label: string}, ref) {const { label, ...otherProps } = props;return (<label>{label}<input {...otherProps} /></label>);});
forwardRef() 4. Ref
Speaking of ref, letâs take a look at the type of useRef()
tsxfunctionA () {constref =useRef <HTMLParagraphElement >(null)return <p ref ={ref }/>}
tsxfunctionA () {constref =useRef <HTMLParagraphElement >(null)return <p ref ={ref }/>}
Sometimes when we want to use useRef to create stable value wrappers but we get âŚ
tsxfunctionA () {constref =useRef <boolean>(null)Cannot assign to 'current' because it is a read-only property.2540Cannot assign to 'current' because it is a read-only property.ref .= false current }
tsxfunctionA () {constref =useRef <boolean>(null)Cannot assign to 'current' because it is a read-only property.2540Cannot assign to 'current' because it is a read-only property.ref .= false current }
Wait, I see above errors all the time!
This is because the type definition of useRef() has overloads,
TypeScript tries to match one from top to bottom according to the calling signature.
tsfunction useRef<T>(initialValue: T): MutableRefObject<T>;function useRef<T>(initialValue: T|null): RefObject<T>;function useRef<T = undefined>(): MutableRefObject<T | undefined>;
tsfunction useRef<T>(initialValue: T): MutableRefObject<T>;function useRef<T>(initialValue: T|null): RefObject<T>;function useRef<T = undefined>(): MutableRefObject<T | undefined>;
The first definition says the generic and the argument type must be the same.
Since we are passing boolean to the generics, and the argument is null,
the second definition rather than the first one matches, which means ref is RefObject<boolean> and thus current is readonly.
tsinterface RefObject<T> {readonly current: T | null;}
tsinterface RefObject<T> {readonly current: T | null;}
It is simple to fix, explicitly type it with boolean | null.
tsxfunctionA () {constref =useRef <boolean | null>(null)ref .current = false}
tsxfunctionA () {constref =useRef <boolean | null>(null)ref .current = false}
And now the first overload is matched and ref is now MutableRefObject.
By the way, HTMLParagraphElement might look a bit exotic, ElementRef<'p'> could be simpler.
tsxfunctionA () {constref1 =useRef <HTMLParagraphElement >(null)constref2 =useRef <ElementRef <'p'>>(null)}
tsxfunctionA () {constref1 =useRef <HTMLParagraphElement >(null)constref2 =useRef <ElementRef <'p'>>(null)}
5. ComponentProps
ComponentProps<T> is useful if we want to retrieve the props type from a component,
especially when we want to reuse the type of child component in parent component.
tsxfunctionComponent (props : {a : 'JSer'}) {return <p >...</p >}ÂtypeA =ComponentProps <typeofComponent >['a']Â// Now from Parent, we get a transparent type for the props we need to pass down.functionParent ({a }: {a :A }) {return <Component a ={a }/>}
tsxfunctionComponent (props : {a : 'JSer'}) {return <p >...</p >}ÂtypeA =ComponentProps <typeofComponent >['a']Â// Now from Parent, we get a transparent type for the props we need to pass down.functionParent ({a }: {a :A }) {return <Component a ={a }/>}
The implementation is pretty straightforward with infer.
As I mentioned before in videos about TypeScript, we can think of
TypeScriptâs type system as regular expressions + recursions.
tstype ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =T extends JSXElementConstructor<infer P>? P: T extends keyof JSX.IntrinsicElements? JSX.IntrinsicElements[T]: {};
tstype ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =T extends JSXElementConstructor<infer P>? P: T extends keyof JSX.IntrinsicElements? JSX.IntrinsicElements[T]: {};
6. useState()
One of the most important hooks is useState(), as it is the only stateful hook we have.
Letâs look at the typings of the setter.
tsxfunctionA () {const [state ,setState ] =useState (0)}
tsxfunctionA () {const [state ,setState ] =useState (0)}
It is quite long, but actually pretty simple.
We know that setState() accept a function or a value.
tstype SetStateAction<S> = S | ((prevState: S) => S);type Dispatch<A> = (value: A) => void;
tstype SetStateAction<S> = S | ((prevState: S) => S);type Dispatch<A> = (value: A) => void;
7. Synthetic Event
From my experience, another pain point in React typings is the event handler. Sometimes it just doesnât fit very well.
tsxfunctionA () {constParameter 'e' implicitly has an 'any' type.7006Parameter 'e' implicitly has an 'any' type.handler = () => { e console .log (e .currentTarget )}Âreturn <button onClick ={handler }>click</button >}
tsxfunctionA () {constParameter 'e' implicitly has an 'any' type.7006Parameter 'e' implicitly has an 'any' type.handler = () => { e console .log (e .currentTarget )}Âreturn <button onClick ={handler }>click</button >}
We can type the Synthetic Event.
tsximport {MouseEvent } from 'react'ÂfunctionA () {consthandler = (e :MouseEvent <HTMLButtonElement >) => {console .log (e .currentTarget )}Âreturn <button onClick ={handler }>click</button >}
tsximport {MouseEvent } from 'react'ÂfunctionA () {consthandler = (e :MouseEvent <HTMLButtonElement >) => {console .log (e .currentTarget )}Âreturn <button onClick ={handler }>click</button >}
Or just type the handler. You can hover the event handler in JSX to see the type it requires.
tsximport {MouseEventHandler } from 'react'ÂfunctionA () {consthandler :MouseEventHandler <HTMLButtonElement > = (e ) => {console .log (e .currentTarget )}Âreturn <button onClick ={handler }>click</button >}
tsximport {MouseEventHandler } from 'react'ÂfunctionA () {consthandler :MouseEventHandler <HTMLButtonElement > = (e ) => {console .log (e .currentTarget )}Âreturn <button onClick ={handler }>click</button >}
Alright, thatâs pretty much of it for this post. Hope it helps. Let me know if you need explanations on more React types.
Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!
