React with TypeScript: The Types You Actually Need

In React with TypeScript you mostly type a component's props, and inference handles the rest. Describe props with an interface, use React.ReactNode for children, pass a type argument to useState when the start value is too narrow, and pick event types such as React.ChangeEvent<HTMLInputElement> for handlers. Files with JSX use the .tsx extension.

The types used most
Thing to typeType
Anything renderable as childrenReact.ReactNode
Exactly one JSX elementReact.ReactElement
Input change handlerReact.ChangeEvent<HTMLInputElement>
Button click handlerReact.MouseEvent<HTMLButtonElement>
Form submit handlerReact.FormEvent<HTMLFormElement>
Inline style propReact.CSSProperties
All props of a native elementReact.ComponentProps<"button">

Updated

Search the cheat sheet for the thing you are trying to type, such as a click handler or a ref, and copy the line. Longer worked examples for context, reducers and generic components follow.

React TypeScript cheat sheet

Worked examples

A component with typed props and children

Optional props get defaults in the signature; children accepts anything renderable.

interface CardProps {
  title: string;
  tone?: "neutral" | "success";
  children: React.ReactNode;
}

export function Card({ title, tone = "neutral", children }: CardProps) {
  return (
    <section className={tone === "success" ? "card card-ok" : "card"}>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

Context with a safe hook

The hook throws a clear error outside the provider and returns a non-null value inside it.

import { createContext, useContext, useState } from "react";

type Theme = "light" | "dark";
interface ThemeValue { theme: Theme; toggle: () => void }

const ThemeContext = createContext<ThemeValue | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");
  const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
  return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>;
}

export function useTheme() {
  const value = useContext(ThemeContext);
  if (!value) throw new Error("useTheme must be used inside ThemeProvider");
  return value;
}

useReducer with a discriminated union

Inside each case the action is narrowed, so action.item only exists where it should.

interface Item { id: string; name: string }
interface State { items: Item[] }
type Action = { type: "add"; item: Item } | { type: "remove"; id: string } | { type: "clear" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "add": return { items: [...state.items, action.item] };
    case "remove": return { items: state.items.filter((i) => i.id !== action.id) };
    case "clear": return { items: [] };
  }
}

// const [state, dispatch] = useReducer(reducer, { items: [] });

A generic list component

T is fixed by the items the caller passes, so renderItem receives the right type.

interface ListProps<T> {
  items: T[];
  getKey: (item: T) => string;
  renderItem: (item: T) => React.ReactNode;
}

export function List<T>({ items, getKey, renderItem }: ListProps<T>) {
  return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}

// <List items={users} getKey={(u) => u.id} renderItem={(u) => u.name} />

Focusing an input through a ref prop

The parent creates the ref; the child places it on the real input.

import { useRef } from "react";

function TextField({ label, ref }: { label: string; ref?: React.Ref<HTMLInputElement> }) {
  return <label>{label} <input ref={ref} /></label>;
}

export function Search() {
  const inputRef = useRef<HTMLInputElement>(null);
  return (
    <>
      <TextField label="Search" ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
    </>
  );
}

Props first, everything else follows

Most type errors in a React codebase start at a component boundary. Write an interface for the props, destructure them in the signature, and the checker validates every place the component is used.

Optional props get a question mark and usually a default in the destructuring, such as size = "md". Union props like variant: "primary" | "ghost" give you autocomplete and reject typos.

Events and forms

Every handler receives a React synthetic event typed by the element it came from. Use event.currentTarget to read the element the handler is attached to; its type comes from the generic argument.

If you prefer to type the whole handler, React exports handler types too, for example React.ChangeEventHandler<HTMLInputElement>.

Generic components

A list or select that works with any item type can take a type parameter. The caller's data fixes the type, and render props receive correctly typed items without casts.

How it works

  1. Type a word such as event, ref or children into the search box.
  2. Narrow by group if the list is long.
  3. Copy the snippet next to the matching row.
  4. Read the worked examples below for patterns that need more than one line.

Tips

Frequently asked questions

How do I type children?

Add children: React.ReactNode to the props interface. ReactNode covers strings, numbers, elements, arrays and null. Use React.ReactElement only when the component must receive a single JSX element.

Why is my useState value typed as never[]?

An empty array gives the checker nothing to infer from. Pass the element type: useState<string[]>([]). The same applies to null starting values, as in useState<User | null>(null).

How do I type a ref to a DOM element?

Call useRef<HTMLInputElement>(null) and attach it with ref={inputRef}. Read inputRef.current with a null check, because it is null until React mounts the element and again after it unmounts.

How do I pass a ref into my own component?

The current React documentation shows a function component receiving ref as a regular prop and placing it on the inner element. Type that prop as React.Ref<HTMLInputElement>. Older code wraps the component in forwardRef instead.

What should createContext default to?

When there is no sensible default, use createContext<Value | null>(null) and wrap useContext in a small hook that throws if the value is null. Every consumer then gets a non-null type and a clear error if a provider is missing.

Related: TypeScript interfaces, TypeScript vs JavaScript, React hooks cheat sheet, React Hook Form examples, Data fetching in React.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap