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>
.
tsx
functionC () {return <p >aa</p >}constelement = <C />
tsx
functionC () {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.
ts
type ReactFragment = Iterable<ReactNode>;
ts
type 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.
tsx
importReact , {Fragment } from 'react'constel = <Fragment ><p /></Fragment >
tsx
importReact , {Fragment } from 'react'constel = <Fragment ><p /></Fragment >
While for ReactPortal
, it extends ReactElement
, but with children
hoisted on root level.
ts
interface ReactPortal extends ReactElement {key: Key | null;children: ReactNode;}
ts
interface 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
children
resides 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
children
resides 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()
tsx
functionA () {constref =useRef <HTMLParagraphElement >(null)return <p ref ={ref }/>}
tsx
functionA () {constref =useRef <HTMLParagraphElement >(null)return <p ref ={ref }/>}
Sometimes when we want to use useRef
to create stable value wrappers but we get âŚ
tsx
functionA () {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 }
tsx
functionA () {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.
ts
function useRef<T>(initialValue: T): MutableRefObject<T>;function useRef<T>(initialValue: T|null): RefObject<T>;function useRef<T = undefined>(): MutableRefObject<T | undefined>;
ts
function 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.
ts
interface RefObject<T> {readonly current: T | null;}
ts
interface RefObject<T> {readonly current: T | null;}
It is simple to fix, explicitly type it with boolean | null
.
tsx
functionA () {constref =useRef <boolean | null>(null)ref .current = false}
tsx
functionA () {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.
tsx
functionA () {constref1 =useRef <HTMLParagraphElement >(null)constref2 =useRef <ElementRef <'p'>>(null)}
tsx
functionA () {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.
tsx
functionComponent (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 }/>}
tsx
functionComponent (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.
ts
type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =T extends JSXElementConstructor<infer P>? P: T extends keyof JSX.IntrinsicElements? JSX.IntrinsicElements[T]: {};
ts
type 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.
tsx
functionA () {const [state ,setState ] =useState (0)}
tsx
functionA () {const [state ,setState ] =useState (0)}
It is quite long, but actually pretty simple.
We know that setState()
accept a function or a value.
ts
type SetStateAction<S> = S | ((prevState: S) => S);type Dispatch<A> = (value: A) => void;
ts
type 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.
tsx
functionA () {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 >}
tsx
functionA () {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.
tsx
import {MouseEvent } from 'react'ÂfunctionA () {consthandler = (e :MouseEvent <HTMLButtonElement >) => {console .log (e .currentTarget )}Âreturn <button onClick ={handler }>click</button >}
tsx
import {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.
tsx
import {MouseEventHandler } from 'react'ÂfunctionA () {consthandler :MouseEventHandler <HTMLButtonElement > = (e ) => {console .log (e .currentTarget )}Âreturn <button onClick ={handler }>click</button >}
tsx
import {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!