React Hooks Typing

Type-safe state management.

TypeScript5 min readConcept 52 of 54

Typing State

For simple state, TypeScript infers the type automatically: const [count, setCount] = useState(0) infers number.

But for complex state, especially when using useReducer, you must explicitly define the shapes of all possible state transitions.

The Optional Payload Trap

A common mistake when typing a reducer action is type Action = { type: string, payload?: any }.

This is dangerous! It allows you to dispatch { type: 'INCREMENT', payload: 'foo' }, even though incrementing shouldn't take a payload, and certainly not a string.

The correct pattern is a **Discriminated Union**.

Syntax

tsx // 1. Define exact action shapes type Action = | { type: "INCREMENT" } | { type: "DECREMENT" } | { type: "SET_VALUE"; payload: number }; // 2. Type the Reducer function reducer(state: number, action: Action) { switch (action.type) { case "INCREMENT": return state + 1; case "SET_VALUE": return action.payload; // Type-safe! } }

Try it

Select an action type to dispatch, and watch how the Discriminated Union instantly restricts whether a payload is allowed, required, or forbidden.

1. Select Action.type
2. Include Payload?
// 1. The Discriminated Union
type Action =
| { type: "INCREMENT" }
| { type: "SET_VALUE", payload: number };

// 2. The Dispatch
dispatch({
type: "INCREMENT",
});
TypeScript Compiler
Valid Dispatch
The INCREMENT action explicitly forbids a payload. You correctly omitted it.
Try breaking the rules!

Check yourself

Pick an answer to lock it in, then read why. Getting one wrong is part of how it sticks.

  1. 1Why is a Discriminated Union better than making `payload` optional for `useReducer` actions?

Remember this

  • Simple useState(0) is auto-inferred.
  • Complex useState<User | null>(null) requires a generic.
  • Always type useReducer actions using a Discriminated Union.
  • Always initialize DOM useRef with null.

Done with this concept?

Mark it complete to track your progress. No login needed.