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
  2. ReactNode
  3. FunctionComponent
  4. Ref
  5. ComponentProps
  6. useState
  7. Synthetic Event

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
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 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
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 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 name
interface 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 name
interface 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
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 we want to use useRef to create stable value wrappers.

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 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
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 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
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 one state hook we have. Let’s see 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 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?