Going DeeperPro· 40 min read

TypeScript with React: Typing Props & State

Most people learn TypeScript to use it with React. Here is the bridge: how to type a component’s props and its useState hook.

What you will learn

  • Type a function component’s props with an interface
  • Type the useState hook correctly
  • Type an event handler in a React component

Why React + TypeScript is the common pairing

React components pass data around as props (the inputs a component receives) and hold changing values as state. Without types it is easy to pass the wrong prop or misuse state. TypeScript makes both safe and gives you autocomplete inside your components. This lesson is a bridge — our full React course goes deeper, but here is the core typing you need.

These snippets are .tsx files — TSX is TypeScript with JSX (the HTML-like syntax React uses). They run after a build step (Vite, Next.js, etc.), so we show them with their rendered Output.

Typing props with an interface

Describe a component’s props with an interface, then annotate the function’s parameter with it. TypeScript now checks every place the component is used.

A typed React component with required and optional props
interface GreetingProps {
  name: string;
  excited?: boolean;     // optional prop
}

function Greeting(props: GreetingProps) {
  return <h1>Hello, {props.name}{props.excited ? '!' : '.'}</h1>;
}

// Using it:
// <Greeting name="Asha" excited />        works
// <Greeting name={42} />                  error: name must be string
// <Greeting excited />                    error: name is required

Note: Output (rendered): Hello, Asha! GreetingProps makes name required and excited optional. Using <Greeting name={42} /> is caught — name must be a string. Forgetting name entirely is also caught. The component’s contract is now enforced everywhere it is used.

Destructuring props (the common style)

Most React code pulls the props apart in the parameter list. The type goes on the destructured object:

Destructured props typed with an interface
interface ButtonProps {
  label: string;
  onClick: () => void;     // a function that takes nothing, returns nothing
}

function Button({ label, onClick }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

Note: Output (rendered): A clickable button showing the label text. The onClick prop is typed () => void, so passing something that is not a function is an error. Destructuring { label, onClick } is the same props object — just unpacked for convenience.

Typing useState

The useState hook holds a piece of state. TypeScript usually infers the type from the initial value, so useState(0) is already a number. You write the type explicitly with the <...> angle brackets when the initial value alone is not enough — most often when state can also be null.

useState inferred for a number, explicit for a nullable value
import { useState } from 'react';

function Counter() {
  // Inferred: count is number, setCount takes a number
  const [count, setCount] = useState(0);

  // Explicit: a user that starts as null
  const [user, setUser] = useState<string | null>(null);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

Note: Output (rendered): A button reading "Clicked 0 times" that counts up on each click. useState(0) infers count as number, so setCount('x') would error. useState<string | null>(null) needs the explicit type because null alone does not tell TypeScript the eventual value is a string. The angle brackets pass that type in — just like any generic.

Typing an event handler

Event handlers receive a typed event object. React provides the event types; you annotate the parameter so e.target.value and friends are checked.

A change event typed with React.ChangeEvent
import { useState } from 'react';

function NameInput() {
  const [name, setName] = useState('');

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setName(e.target.value);   // e.target.value is typed as string
  }

  return <input value={name} onChange={handleChange} />;
}

Note: Output (rendered): A text box whose value updates as you type. React.ChangeEvent<HTMLInputElement> types the event so e.target.value is a known string. Without the type, e would be any and a typo like e.target.valeu would slip through.

The React-typing essentials

You are typing...Use
Component propsAn interface on the function parameter
A number/string/boolean stateLet useState infer it
State that can be nullExplicit useState<T | null>(null)
A change eventReact.ChangeEvent<HTMLInputElement>
A click eventReact.MouseEvent<HTMLButtonElement>

Tip: Two habits cover most React typing: define a ...Props interface for every component, and let useState infer unless the value can be null/empty (then pass the type explicitly). Our full React course builds on exactly this foundation.

Watch out: A very common mistake: useState(null) with no type. TypeScript infers the type as just null, so you can never set anything else into it. When state starts empty, give it the real type up front: useState<User | null>(null).

Q. You write const [user, setUser] = useState(null) for state that will later hold a User object. What goes wrong?

Answer: With only null as the initial value, useState infers the type as null. You must write useState<User | null>(null) so the state can also hold a User.

✍️ Practice

  1. Write a Card component with a CardProps interface (title: string, subtitle?: string) and render it.
  2. Make a Toggle component that uses useState(false) and flips on click; confirm the inferred boolean type.

🏠 Homework

  1. Build a small typed TodoItem component: a TodoItemProps interface (id: number, text: string, done: boolean, onToggle: () => void). Note which props are required and how the onToggle type prevents passing a non-function.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →