Typing hooks, reducers and context in React
Type useState with unions, useReducer with discriminated actions, useRef for DOM nodes and mutable values, context with a non-null guard hook, and custom hooks that return tuples.
Comes up in: React, Coding
Learn#
Deepa’s checkout wizard dispatches { type: "setAddres" } with a typo. Nothing happens on screen, and nobody notices until a customer’s address is empty.
A reducer is a function (state, action) => newState. An action describes what happened. Type actions as a discriminated union (TS.02.02), and TypeScript checks every dispatch call:
import { useReducer, useRef, useState } from "react";
type Step = "address" | "payment" | "review";
type Action =
| { type: "next" }
| { type: "back" }
| { type: "setAddress"; address: string };
type State = { step: Step; address: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "next": return { ...state, step: state.step === "address" ? "payment" : "review" };
case "back": return { ...state, step: "address" };
case "setAddress": return { ...state, address: action.address };
}
}
export function useCheckout() {
const [state, dispatch] = useReducer(reducer, { step: "address", address: "" });
const [status, setStatus] = useState<"idle" | "saving">("idle");
const input = useRef<HTMLInputElement>(null);
dispatch({ type: "setAddress" });
dispatch({ type: "jump" });
setStatus("done");
input.current.focus();
return [state, dispatch] as const;
}example.ts(22,12): error TS2345: Argument of type '{ type: "setAddress"; }' is not assignable to parameter of type 'Action'.
Property 'address' is missing in type '{ type: "setAddress"; }' but required in type '{ type: "setAddress"; address: string; }'.
example.ts(23,14): error TS2322: Type '"jump"' is not assignable to type '"next" | "back" | "setAddress"'.
example.ts(24,13): error TS2345: Argument of type '"done"' is not assignable to parameter of type 'SetStateAction<"idle" | "saving">'.
example.ts(25,3): error TS18047: 'input.current' is possibly 'null'.Give useState an explicit union when the first value is narrower than the type, and check ref.current before using it.
State
- dispatch: (action: Action) => void
Console
empty
dispatch's parameter type is Action, the union of every action the reducer handles.
Read the steps as text
- dispatch's parameter type is Action, the union of every action the reducer handles.
- { type: "setAddress" } matches the setAddress member by its tag, which requires address. Error.
- "jump" matches no member's tag. Error, before any user clicks anything.
- Inside the reducer, each case narrows action, so action.address is allowed only in setAddress.
Predict#
Predict first
const [user, setUser] = useState(null). Later: setUser({ name: 'Asha' }). What does TypeScript say?
useState(null) infers the state type from null alone. Write useState<User | null>(null). People expect TypeScript to look ahead at later calls, but inference uses only the initial value.
Interview angle#
Say it like this — about 30 seconds
“For useReducer I type the actions as a discriminated union on type, and the reducer switches on it, so dispatch only accepts valid actions with their payloads. useState needs an explicit union when the initial value is null or narrower than the real type. For refs, useRef of HTMLInputElement with null gives a DOM ref that can be null; a mutable ref holds any value. For context, I create it with undefined and export a hook that throws if it is missing, so consumers never see undefined.”
Follow-up How do you type useReducer actions safely?
type, and a never check in the default branch.Follow-up How do you type a context so consumers never get undefined?
createContext<Value | undefined>(undefined) plus a useValue() hook that throws when the value is undefined and returns Value.Follow-up Why return as const from a custom hook?
[value, setValue] is a tuple with fixed positions, not an array of a union of both types.Practice#
Guided
Write the reducer for a four-step checkout wizard. The types are given; the tests check every transition.
Done when
- “next” checks the current step’s field and sets a clear error message when it is missing.
- “back” moves one step back, but never from “account” or “done”.
- The reducer never mutates the old state.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
How do you type useState that starts as null?
useState<User | null>(null)
How should useReducer actions be typed?
As a discriminated union on a type field.
Why does useRef<HTMLInputElement>(null).current need a check?
It is null until React attaches the element.
How do you make a context safe to consume?
Default to undefined and read it through a hook that throws if missing.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
useState infers from its initial value: useState(0) is number, useState<"idle" | "saving">("idle") needs the explicit union. A lazy initializer, useState(() => load()), infers from the function’s return type.
Check yourself: What type does useState(0) infer?
number
How it works underneathSenior rounds
useRef always needs an argument. useRef<HTMLInputElement>(null) gives a current that can be null, for DOM nodes. useRef<number>(0) is a mutable box for values such as timer IDs Source: React: useRef.Check yourself: Which hook type do you use for a DOM ref in React 19?
useRef<HTMLInputElement>(null), giving RefObject<HTMLInputElement | null>.
Edge cases and trapsWhere people slip
undefined and export a guard hook: const v = useContext(Ctx); if (!v) throw new Error("useAuth must be inside AuthProvider"); return v;. Every consumer then gets the non-null type Source: React: createContext.Check yourself: Why is createContext({} as Value) dangerous?
Consumers outside a provider get {} typed as Value and crash at runtime.
In productionStaff-level answers
as const, so callers get a tuple, not (A | B)[].Check yourself: What does return [value, set] as const give a hook's callers?
A readonly tuple: position 0 and 1 keep their own types.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- How do you type
useReduceractions safely? - How do you type a context so consumers never get
undefined?