TypeScript · Projects

TypeScript with React

React + TypeScript is the most popular front-end combination today. Typed props mean your editor knows every component’s API, and mistakes show up before you open the browser.

Advanced 6 min Lesson 27 of 29Examples target TypeScript 5.x with "strict": true.
0/29 in TypeScript
On this page · 6 min · Advanced

Learning objectives#

  • Type component props and children
  • Type useState, useRef and useReducer
  • Type event handlers and forms
  • Write a typed custom hook
  • Type a context provider

Explanation#

Props are an interface or type: function Button({ label, onClick }: ButtonProps). Optional props use ?; defaults go in destructuring.

children: React.ReactNode accepts anything renderable. Extend native props with React.ComponentProps<"button"> to accept every normal button attribute.

useState infers from the initial value; give it a type when it starts empty: useState<User | null>(null), useState<string[]>([]).

useRef<HTMLInputElement>(null) for DOM refs. Events: React.ChangeEvent<HTMLInputElement>, React.FormEvent<HTMLFormElement>, React.MouseEvent<HTMLButtonElement>.

useReducer works best with a discriminated union of actions. Context: createContext<Value | null>(null) plus a hook that throws if used outside the provider.

Syntax#

TSX Illustrative
interface Props { title: string; count?: number }
function Badge({ title, count = 0 }: Props) {
  return <span>{title}: {count}</span>;
}

Code example: A typed to-do component#

TSXTodoApp.tsx Illustrative
import { useReducer, useRef, type ReactNode, type FormEvent, type ComponentProps } from 'react';


interface Todo { id: number; text: string; done: boolean }


type Action =
  | { type: 'add'; text: string }
  | { type: 'toggle'; id: number }
  | { type: 'remove'; id: number };


function reducer(todos: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case 'add':    return [...todos, { id: Date.now(), text: action.text, done: false }];
    case 'toggle': return todos.map(t => (t.id === action.id ? { ...t, done: !t.done } : t));
    case 'remove': return todos.filter(t => t.id !== action.id);
  }
}


type ButtonProps = ComponentProps<'button'> & { variant?: 'primary' | 'danger' };
function Button({ variant = 'primary', ...rest }: ButtonProps) {
  return <button className={`btn btn-${variant}`} {...rest} />;
}


function Card({ title, children }: { title: string; children: ReactNode }) {
  return <section><h2>{title}</h2>{children}</section>;
}


export function TodoApp() {
  const [todos, dispatch] = useReducer(reducer, []);
  const inputRef = useRef<HTMLInputElement>(null);


  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const text = inputRef.current?.value.trim();
    if (!text) return;
    dispatch({ type: 'add', text });
    inputRef.current!.value = '';
  }


  return (
    <Card title={`To-do (${todos.length})`}>
      <form onSubmit={handleSubmit}>
        <input ref={inputRef} aria-label="New task" />
        <Button type="submit">Add</Button>
      </form>
      <ul>
        {todos.map(t => (
          <li key={t.id}>
            <label>
              <input type="checkbox" checked={t.done} onChange={() => dispatch({ type: 'toggle', id: t.id })} />
              {t.text}
            </label>
            <Button variant="danger" onClick={() => dispatch({ type: 'remove', id: t.id })}>Delete</Button>
          </li>
        ))}
      </ul>
      {/* <Button variant="warning" />  ✗ Type '"warning"' is not assignable to type '"primary" | "danger"' */}
      {/* dispatch({ type: 'toggle' })  ✗ Property 'id' is missing */}
    </Card>
  );
}
Expected output
A working to-do list: type a task and press Add, tick it off, or delete it. Invalid button variants and malformed actions are compile errors.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Todo and Action describe the data; Action is a discriminated union.
  2. reducer’s switch narrows each action, so action.text exists only for "add".
  3. ButtonProps reuses every native button prop and adds variant.
  4. Card types children as ReactNode.
  5. useRef<HTMLInputElement> types the input ref; FormEvent types the submit handler.
  6. Invalid props and actions are caught by the compiler.

Real-world use case#

Every component library (MUI, Chakra, shadcn/ui) ships typed props, so your editor autocompletes them and flags invalid combinations.

Common mistakes#

useState([]) without a type

An empty array infers never[], and you cannot add anything. Write useState<Todo[]>([]).

Typing events as any

Use the specific React event types; hover over onChange in your editor to see the exact type.

Using React.FC everywhere

Plain functions with typed props are simpler and are the common recommendation today.

Best practices#

  • Type props with an interface or type next to the component
  • Extend native element props with ComponentProps<"tag">
  • Type empty and nullable state explicitly
  • Use discriminated unions for reducer actions
  • Make context hooks throw when used outside their provider

Remember this#

Props = an interface. children = ReactNode.

Empty state? Tell it the type: useState<T[]>([]).

Hover the handler to learn the event type.

Practice exercise#

Your task: Create a typed Avatar component with props src (string), alt (string), size ("sm" | "md" | "lg", default "md") and optional onClick.

Need a hint?
Use an interface and a default in destructuring.
Show solution
TSX Illustrative
interface AvatarProps {
  src: string;
  alt: string;
  size?: 'sm' | 'md' | 'lg';
  onClick?: () => void;
}
const sizes = { sm: 32, md: 48, lg: 64 };


export function Avatar({ src, alt, size = 'md', onClick }: AvatarProps) {
  const px = sizes[size];
  return <img src={src} alt={alt} width={px} height={px} onClick={onClick} style={{ borderRadius: '50%' }} />;
}

Mini quiz#

TypeScript with React quizMultiple choice Question 1 of 3

How do you type state that starts as null but will hold a User?

Score so far: 0/0

Want more? Practise all TypeScript MCQs · TypeScript interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.