TypeScript with React

Learn how to use TypeScript with React to build safer components, typed props, reliable state, and refactor-friendly UI code. This guide covers practical patterns used in production React apps.

On this page

Why TypeScript with React?

React applications grow fast: components multiply, props evolve, and state becomes complex. TypeScript adds strong guarantees to your UI layer by making component APIs explicit. In production, this reduces bugs, improves refactoring confidence, and makes components easier to reuse correctly.

Core idea: components are contracts

In React, props are the public API of a component. TypeScript lets you define that API precisely, so incorrect usage is caught immediately by the editor and build pipeline.

Typing props in function components

Define a props type (or interface) and use it in the component signature.

type ButtonProps = {
  label: string;
  onClick: () => void;
  disabled?: boolean;
};

function Button({ label, onClick, disabled = false }: ButtonProps) {
  return (
    
  );
}

Typing children

Many components accept children. Use React.ReactNode when you need to accept any renderable content.

type CardProps = {
  title: string;
  children: React.ReactNode;
};

function Card({ title, children }: CardProps) {
  return (
    

{title}

{children}
); }

Typing useState

TypeScript often infers state types, but explicit typing is helpful when the initial value is null or when the state is complex.

const [count, setCount] = React.useState(0); // inferred as number

type User = { id: number; name: string };
const [user, setUser] = React.useState(null);

Typing event handlers

React events are synthetic events. Use the correct event types for inputs and forms.

function SearchBox() {
  const [q, setQ] = React.useState("");

  function onChange(e: React.ChangeEvent) {
    setQ(e.target.value);
  }

  return ;
}

Typing useRef

Refs can point to DOM elements or hold mutable values. DOM refs should include null because they are assigned after render.

const inputRef = React.useRef(null);

function focus() {
  inputRef.current?.focus();
}

Typing async data and API responses

Model API responses as types and keep component state aligned. Always treat external data as untrusted and validate where necessary.

type User = { id: number; name: string };

async function fetchUsers(): Promise {
  const res = await fetch("/api/users");
  const data = await res.json();
  return data as User[];
}

Discriminated unions for UI state

One of the best production patterns is modeling UI state with discriminated unions. This makes impossible states unrepresentable.

type LoadState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; message: string };

const [state, setState] = React.useState({ status: "idle" });

Props vs derived values

Avoid over-typing derived values. Type your boundaries (props, API, shared utils) and let inference handle internals. This keeps components readable while still safe.

Common mistakes

  • Using any for props or API data to silence errors.
  • Not typing nullable state and then assuming values exist.
  • Overusing React.FC without understanding children and defaultProps implications.
  • Forcing types with assertions instead of modeling the state correctly.

Production guidance

  • Type component props explicitly (public API).
  • Use discriminated unions for async/loading/error UI states.
  • Prefer inference for local variables, be explicit at boundaries.
  • Avoid any; use unknown + narrowing or runtime validation for external data.
  • Keep types simple and readable for the team.

What’s next

Next, we should cover tooling and build workflow for real projects: scripts, watch mode, linting, and a practical migration strategy for adding TypeScript to existing React codebases.