Skip to main content

Types

Props Types

export declare interface AppProps {
children: React.ReactNode // best
style?: React.CSSProperties // for style
onChange?: (e: React.FormEvent<HTMLInputElement>) => void // form events!
props: Props & React.HTMLProps<HTMLButtonElement>
}

React Refs Types

class CssThemeProvider extends React.PureComponent<Props> {
private rootRef: React.RefObject<HTMLDivElement> = React.createRef()

render() {
return <div ref={this.rootRef}>{this.props.children}</div>
}
}

Function Component Types

Don't use React.FC/React.FunctionComponent:

  • React 17: Unnecessary addition of children (hide some run-time error).
  • React 18: @types/react v18 remove implicit children in React.FunctionComponent.
  • React.FC doesn't support generic components.
  • Barrier for <Comp> with <Comp.Sub> types (component as namespace pattern).
  • React.FC doesn't work correctly with defaultProps.
// Declaring type of props
interface Props {
message: string
}

// Inferred return type
const Message = ({ message }: Props) => <div>{message}</div>

// Explicit return type annotation
const Message = ({ message }: Props): JSX.Element => <div>{message}</div>

// Inline types annotation
const Message = ({ message }: { message: string }) => <div>{message}</div>

export default function App() {
return <Message message="message" />
}

Class Component Types

  • React.Component<P, S>
  • readonly state: State
  • static defaultProps
  • static getDerivedStateFromProps
class MyComponent extends React.Component<{
message?: string
}> {
render() {
const { message = 'default' } = this.props
return <div>{message}</div>
}
}
import Button from './Button'

type Props = typeof ButtonCounter.defaultProps & {
name: string
}

const initialState = { clicksCount: 0 }
type State = Readonly<typeof initialState>

class ButtonCounter extends React.Component<Props, State> {
readonly state: State = initialState

static defaultProps = {
name: 'count',
}

static getDerivedStateFromProps(
props: Props,
state: State,
): Partial<State> | null {
// ...
}

render() {
return <span>{this.props.foo}</span>
}
}

Generic Component Types

// 一个泛型组件
interface SelectProps<T> {
items: T[]
}

class Select<T> extends React.Component<SelectProps<T>, any> {}

// 使用
const Form = () => <Select<string> items={['a', 'b']} />

export default function App() {
return <Form />
}

In .tsx file, <T> maybe considered JSX.Element, use extends {} to avoid it:

const foo = <T extends object>(arg: T) => arg

Component Props Type

  • React.ComponentProps
  • React.ComponentPropsWithRef
  • React.ComponentPropsWithoutRef
import { Button } from 'library'

type ButtonProps = React.ComponentProps<typeof Button>
type AlertButtonProps = Omit<ButtonProps, 'onClick'>

const AlertButton: React.FC<AlertButtonProps> = props => (
<Button onClick={() => alert('hello')} {...props} />
)

export default function App() {
return <AlertButton />
}

Typing existing untyped React components:

declare module 'react-router-dom' {
import * as React from 'react'

interface NavigateProps<T> {
to: string | number
replace?: boolean
state?: T
}

export class Navigate<T = any> extends React.Component<NavigateProps<T>> {}
}

Component Return Type

  • JSX.Element: return value of React.createElement.
  • React.ReactNode: return value of a component.
function foo(bar: string) {
return { baz: 1 }
}

type FooReturn = ReturnType<typeof foo> // { baz: number }

React Event Types

  • React.SyntheticEvent.
  • React.AnimationEvent: CSS animations.
  • React.ChangeEvent: <input>/<select>/<textarea> change events.
  • React.ClipboardEvent: copy/paste/cut events.
  • React.CompositionEvent: user indirectly entering text events.
  • React.DragEvent: drag/drop interaction events.
  • React.FocusEvent: elements gets/loses focus events.
  • React.FormEvent<HTMLElement>: form focus/change/submit events.
  • React.InvalidEvent: validity restrictions of inputs fails.
  • React.KeyboardEvent: keyboard interaction events.
  • React.MouseEvent: pointing device interaction events (e.g. mouse).
  • React.TouchEvent: touch device interaction events. Extends UIEvent.
  • React.PointerEvent: advanced pointing device interaction events (includes mouse, pen/stylus, touchscreen), recommended for modern browser. Extends UIEvent.
  • React.TransitionEvent: CSS transition. Extends UIEvent.
  • React.UIEvent: base event for Mouse/Touch/Pointer events.
  • React.WheelEvent: mouse wheel scrolling events.
  • Missing InputEvent (extends UIEvent): InputEvent is still an experimental interface and not fully supported by all browsers. Use SyntheticEvent instead.

React Event Handler Types

  • React.ChangeEventHandler<HTMLElement>.

React Form Event Types

interface State {
text: string
}

class App extends React.Component<Props, State> {
state = {
text: '',
}

// typing on RIGHT hand side of =
onChangeEvent = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ text: e.currentTarget.value })
}

// typing on LEFT hand side of =
onChangeHandler: React.ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({ text: e.currentTarget.value })
}

render() {
return (
<div>
<input type="text" value={this.state.text} onChange={this.onChange} />
</div>
)
}
}
export default function Form() {
return (
<form
ref={formRef}
onSubmit={(e: React.SyntheticEvent) => {
e.preventDefault()

const target = e.target as typeof e.target & {
email: { value: string }
password: { value: string }
}

const email = target.email.value // Type Checks
const password = target.password.value // Type Checks
}}
>
<div>
<label>
Email:
<input type="email" name="email" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Log in" />
</div>
</form>
)
}

React HTML and CSS Types

  • React.DOMAttributes<HTMLElement>
  • React.AriaAttributes<HTMLElement>
  • React.SVGAttributes<HTMLElement>
  • React.HTMLAttributes<HTMLElement>
  • React.ButtonHTMLAttributes<HTMLButtonElement>
  • React.HTMLProps<HTMLElement>
  • React.CSSProperties

React Input Types

type StringChangeHandler = (newValue: string) => void
type NumberChangeHandler = (newValue: number) => void
type BooleanChangeHandler = (newValue: boolean) => void

interface BaseInputDefinition {
id: string
label: string
}

interface TextInputDefinition extends BaseInputDefinition {
type: 'text'
value: string
onChange: StringChangeHandler
}

interface NumberInputDefinition extends BaseInputDefinition {
type: 'number'
value: number
onChange: NumberChangeHandler
}

interface CheckboxInputDefinition extends BaseInputDefinition {
type: 'checkbox'
value: boolean
onChange: BooleanChangeHandler
}

type Input
= | TextInputDefinition
| NumberInputDefinition
| CheckboxInputDefinition

React Portal Types

const modalRoot = document.getElementById('modal-root') as HTMLElement

export class Modal extends React.Component<{ children: ReactElement }> {
el: HTMLElement = document.createElement('div')

componentDidMount() {
modalRoot.appendChild(this.el)
}

componentWillUnmount() {
modalRoot.removeChild(this.el)
}

render() {
return ReactDOM.createPortal(this.props.children, this.el)
}
}
import type React from 'react'
import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'

const modalRoot = document.querySelector('#modal-root') as HTMLElement

const Modal: React.FC<object> = ({ children }) => {
const el = useRef(document.createElement('div'))

useEffect(() => {
const current = el.current
modalRoot!.appendChild(current)
return () => modalRoot!.removeChild(current)
}, [])

return createPortal(children, el.current)
}

export default Modal
import { Modal } from '@components'

export default function App() {
const [showModal, setShowModal] = React.useState(false)

return (
<div>
<div id="modal-root"></div>
{showModal && (
<Modal>
<div>
I&apos;m a modal!
{' '}
<button type="button" onClick={() => setShowModal(false)}>
close
</button>
</div>
</Modal>
)}
<button type="button" onClick={() => setShowModal(true)}>
show Modal
</button>
</div>
)
}

React Redux Types

const initialState = {
name: '',
points: 0,
likesGames: true,
}

type State = typeof initialState
export function updateName(name: string) {
return {
type: 'UPDATE_NAME',
name,
} as const
}

export function addPoints(points: number) {
return {
type: 'ADD_POINTS',
points,
} as const
}

export function setLikesGames(value: boolean) {
return {
type: 'SET_LIKES_GAMES',
value,
} as const
}

type Action = ReturnType<
typeof updateName | typeof addPoints | typeof setLikesGames
>

// =>
// type Action = {
// readonly type: 'UPDATE_NAME';
// readonly name: string;
// } | {
// readonly type: 'ADD_POINTS';
// readonly points: number;
// } | {
// readonly type: 'SET_LIKES_GAMES';
// readonly value: boolean;
// }
import type { Reducer } from 'redux'

function reducer(state: State, action: Action): Reducer<State, Action> {
switch (action.type) {
case 'UPDATE_NAME':
return { ...state, name: action.name }
case 'ADD_POINTS':
return { ...state, points: action.points }
case 'SET_LIKES_GAMES':
return { ...state, likesGames: action.value }
default:
return state
}
}

React Hook Types

  • useState<T>
  • Dispatch<T>
  • SetStateAction<T>
  • RefObject<T>
  • MutableRefObject<T>
  • More TypeScript Hooks.

UseState Hook Type

export default function App() {
const [user, setUser] = React.useState<IUser>({} as IUser)
const handleClick = () => setUser(newUser)

return <div>App</div>
}

UseReducer Hook Type

const initialState = { count: 0 }
type State = typeof initialState

type Action
= | { type: 'increment', payload: number }
| { type: 'decrement', payload: string }

function reducer(state: State, action: Action) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload }
case 'decrement':
return { count: state.count - Number(action.payload) }
default:
throw new Error('Error')
}
}

export default function Counter() {
const [state, dispatch] = React.useReducer(reducer, initialState)

return (
<>
Count:
{' '}
{state.count}
<button
type="button"
onClick={() => dispatch({ type: 'decrement', payload: '5' })}
>
-
</button>
<button
type="button"
onClick={() => dispatch({ type: 'increment', payload: 5 })}
>
+
</button>
</>
)
}

UseRef Hook Type

DOM Element Ref Type

  • If possible, prefer as specific as possible.
  • Return type is RefObject<T>.
export default function Foo() {
const divRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (!divRef.current)
throw new Error('divRef is not assigned')

doSomethingWith(divRef.current)
})

return <div ref={divRef}>etc</div>
}

Mutable Value Ref

  • Return type is MutableRefObject<T>.
export default function Foo() {
const intervalRef = useRef<number | null>(null)

// You manage the ref yourself (that's why it's called MutableRefObject!)
useEffect(() => {
intervalRef.current = setInterval()
return () => clearInterval(intervalRef.current)
}, [])

// The ref is not passed to any element's "ref" prop
return (
<button type="button" onClick={() => clearInterval(intervalRef.current)}>
Cancel timer
</button>
)
}

Custom Hooks Types

Use as const type assertion to avoid type inference (especially for [first, second] type).

export function useLoading() {
const [isLoading, setState] = React.useState(false)
const load = () => {
setState(true)
}

// return `[boolean, () => void]` as want
// instead of `(boolean | () => void)[]`
return [isLoading, load] as const
}
import type { Dispatch, SetStateAction } from 'react'
import { useState } from 'react'

interface ReturnType {
value: boolean
setValue: Dispatch<SetStateAction<boolean>>
setTrue: () => void
setFalse: () => void
toggle: () => void
}

function useBoolean(defaultValue?: boolean): ReturnType {
const [value, setValue] = useState(!!defaultValue)

const setTrue = () => setValue(true)
const setFalse = () => setValue(false)
const toggle = () => setValue(x => !x)

return { value, setValue, setTrue, setFalse, toggle }
}

export default useBoolean
import type { RefObject } from 'react'
import { useEffect, useRef } from 'react'

function useEventListener<T extends HTMLElement = HTMLDivElement>(
eventName: keyof WindowEventMap,
handler: (event: Event) => void,
element?: RefObject<T>,
) {
// Create a ref that stores handler
const savedHandler = useRef<(event: Event) => void>()

useEffect(() => {
// Define the listening target
const targetElement: T | Window = element?.current || window
if (!(targetElement && targetElement.addEventListener))
return

// Update saved handler if necessary
if (savedHandler.current !== handler)
savedHandler.current = handler

// Create event listener that calls handler function stored in ref
const eventListener = (event: Event) => {
savedHandler?.current(event)
}

targetElement.addEventListener(eventName, eventListener)

// Remove event listener on cleanup
return () => {
targetElement.removeEventListener(eventName, eventListener)
}
}, [eventName, element, handler])
}

export default useEventListener
import type { AxiosRequestConfig } from 'axios'

import axios from 'axios'
import { useEffect, useReducer, useRef } from 'react'

// State & hook output
interface State<T> {
status: 'init' | 'fetching' | 'error' | 'fetched'
data?: T
error?: string
}

type Cache<T> = Record<string, T>

// discriminated union type
type Action<T>
= | { type: 'request' }
| { type: 'success', payload: T }
| { type: 'failure', payload: string }

function useFetch<T = unknown>(
url?: string,
options?: AxiosRequestConfig,
): State<T> {
const cache = useRef<Cache<T>>({})
const cancelRequest = useRef<boolean>(false)

const initialState: State<T> = {
status: 'init',
error: undefined,
data: undefined,
}

// Keep state logic separated
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
switch (action.type) {
case 'request':
return { ...initialState, status: 'fetching' }
case 'success':
return { ...initialState, status: 'fetched', data: action.payload }
case 'failure':
return { ...initialState, status: 'error', error: action.payload }
default:
return state
}
}

const [state, dispatch] = useReducer(fetchReducer, initialState)

useEffect(() => {
if (!url)
return

const fetchData = async () => {
dispatch({ type: 'request' })

if (cache.current[url]) {
dispatch({ type: 'success', payload: cache.current[url] })
} else {
try {
const response = await axios(url, options)
cache.current[url] = response.data

if (cancelRequest.current)
return

dispatch({ type: 'success', payload: response.data })
} catch (error) {
if (cancelRequest.current)
return

dispatch({ type: 'failure', payload: error.message })
}
}
}

fetchData()

return () => {
cancelRequest.current = true
}
}, [url])

return state
}

export default useFetch