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
function C() {
return <p>aa</p>
}
const element = <C/>
const element: JSX.Element
tsx
function C() {
return <p>aa</p>
}
const element = <C/>
const element: JSX.Element

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
import React, {Fragment} from 'react'
(alias) const Fragment: ExoticComponent<{ children?: ReactNode | undefined; }> import Fragment
const el = <Fragment><p/></Fragment>
const el: JSX.Element
tsx
import React, {Fragment} from 'react'
(alias) const Fragment: ExoticComponent<{ children?: ReactNode | undefined; }> import Fragment
const el = <Fragment><p/></Fragment>
const el: JSX.Element

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 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 under props,

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>
);
});

4. Ref

Speaking of ref, let’s take a look at the type of useRef()

tsx
function A() {
const ref = useRef<HTMLParagraphElement>(null)
const ref: React.RefObject<HTMLParagraphElement>
return <p ref={ref}/>
}
tsx
function A() {
const ref = useRef<HTMLParagraphElement>(null)
const ref: React.RefObject<HTMLParagraphElement>
return <p ref={ref}/>
}

Sometimes when we want to use useRef to create stable value wrappers but we get …

tsx
function A() {
const ref = useRef<boolean>(null)
const ref: React.RefObject<boolean>
ref.current = false
Cannot assign to 'current' because it is a read-only property.2540Cannot assign to 'current' because it is a read-only property.
}
tsx
function A() {
const ref = useRef<boolean>(null)
const ref: React.RefObject<boolean>
ref.current = false
Cannot assign to 'current' because it is a read-only property.2540Cannot assign to 'current' because it is a read-only property.
}

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
function A() {
const ref = useRef<boolean | null>(null)
const ref: React.MutableRefObject<boolean | null>
ref.current = false
}
tsx
function A() {
const ref = useRef<boolean | null>(null)
const ref: React.MutableRefObject<boolean | 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
function A() {
const ref1 = useRef<HTMLParagraphElement>(null)
const ref1: React.RefObject<HTMLParagraphElement>
const ref2 = useRef<ElementRef<'p'>>(null)
const ref2: React.RefObject<HTMLParagraphElement>
}
tsx
function A() {
const ref1 = useRef<HTMLParagraphElement>(null)
const ref1: React.RefObject<HTMLParagraphElement>
const ref2 = useRef<ElementRef<'p'>>(null)
const ref2: React.RefObject<HTMLParagraphElement>
}

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
function Component(props: {a: 'JSer'}) {
return <p>...</p>
}
 
type A = ComponentProps<typeof Component>['a']
type A = "JSer"
 
// Now from Parent, we get a transparent type for the props we need to pass down.
function Parent({a}: {a: A}) {
return <Component a={a}/>
}
tsx
function Component(props: {a: 'JSer'}) {
return <p>...</p>
}
 
type A = ComponentProps<typeof Component>['a']
type A = "JSer"
 
// Now from Parent, we get a transparent type for the props we need to pass down.
function Parent({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
function A() {
const [state, setState] = useState(0)
const setState: React.Dispatch<React.SetStateAction<number>>
}
tsx
function A() {
const [state, setState] = useState(0)
const setState: React.Dispatch<React.SetStateAction<number>>
}

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
function A() {
const handler = (e) => {
Parameter 'e' implicitly has an 'any' type.7006Parameter 'e' implicitly has an 'any' type.
console.log(e.currentTarget)
any
}
 
return <button onClick={handler}>click</button>
}
tsx
function A() {
const handler = (e) => {
Parameter 'e' implicitly has an 'any' type.7006Parameter 'e' implicitly has an 'any' type.
console.log(e.currentTarget)
any
}
 
return <button onClick={handler}>click</button>
}

We can type the Synthetic Event.

tsx
import {MouseEvent} from 'react'
 
function A() {
const handler = (e: MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget)
(property) React.BaseSyntheticEvent<globalThis.MouseEvent, EventTarget & HTMLButtonElement, EventTarget>.currentTarget: EventTarget & HTMLButtonElement
}
 
return <button onClick={handler}>click</button>
}
tsx
import {MouseEvent} from 'react'
 
function A() {
const handler = (e: MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget)
(property) React.BaseSyntheticEvent<globalThis.MouseEvent, EventTarget & HTMLButtonElement, EventTarget>.currentTarget: EventTarget & HTMLButtonElement
}
 
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'
 
function A() {
const handler: MouseEventHandler<HTMLButtonElement> = (e) => {
console.log(e.currentTarget)
(property) React.BaseSyntheticEvent<MouseEvent, EventTarget & HTMLButtonElement, EventTarget>.currentTarget: EventTarget & HTMLButtonElement
}
 
return <button onClick={handler}>click</button>
(property) React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
}
tsx
import {MouseEventHandler} from 'react'
 
function A() {
const handler: MouseEventHandler<HTMLButtonElement> = (e) => {
console.log(e.currentTarget)
(property) React.BaseSyntheticEvent<MouseEvent, EventTarget & HTMLButtonElement, EventTarget>.currentTarget: EventTarget & HTMLButtonElement
}
 
return <button onClick={handler}>click</button>
(property) React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
}

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!

😳 Would you like to share my post to more people ?    

❮ Prev: How does ErrorBoundary work internally in React?

Next: How does useState() work internally in React?