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 by itself, but sometimes when it needs me to add typings, I’m just going 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.
json
{type: Component,props,key,}
json
{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
.
So you can see why the ReactElement is defined as such. 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
here? JSX.Element
is just an alias for ReactElement
. Because JSX itself is
born from React but it is independent from React, projects other than JSX could have different
types even though they share the same 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
Just similar to that in DOM, Element is subtype of Node. 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!
/*** 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!
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 use daily, 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 under props, but Portal only has 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 under props, but Portal only has 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()
?
tsx
import { forwardRef } from 'react';const MyInput = forwardRef(function MyInput(props, ref) {const { label, ...otherProps } = props;return (<label>{label}<input {...otherProps} /></label>);});
tsx
import { forwardRef } from 'react';const MyInput = forwardRef(function MyInput(props, ref) {const { label, ...otherProps } = props;return (<label>{label}<input {...otherProps} /></label>);});
See the second argument is ref ?
ts
// will show `ForwardRef(${Component.displayName || Component.name})` in devtools by default,// but can be given its own specific nameinterface ForwardRefExoticComponent<P> extends NamedExoticComponent<P> {defaultProps?: Partial<P> | undefined;propTypes?: WeakValidationMap<P> | undefined;}function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
ts
// will show `ForwardRef(${Component.displayName || Component.name})` in devtools by default,// but can be given its own specific nameinterface ForwardRefExoticComponent<P> extends NamedExoticComponent<P> {defaultProps?: Partial<P> | undefined;propTypes?: WeakValidationMap<P> | undefined;}function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
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 we want to use useRef
to create stable value wrappers.
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 saw 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>;
Since we are passing boolean
to the generics, and the argument is null
,
the second definition is matched, 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, explicit 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 1st overload is matched and ref
is now MutableRefObject
.
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 one state hook we have.
Let’s see 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 React Internals Deep Dive!