Chapter 2 of 4

Typing Hooks

Where inference is enough and where you need to help it.

useState

TypeScript infers the type from the initial value, which covers most cases. Supply a type argument when the initial value does not describe the full range.

const [count, setCount] = useState(0);          // number
const [name, setName] = useState("");           // string

// Starts as null but will hold a User
const [user, setUser] = useState<User | null>(null);

// An empty array infers never[] without help
const [items, setItems] = useState<Item[]>([]);

useRef

The two uses of useRef need different types, and the difference matters.

// A DOM ref: React writes it, so it is read-only to you
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();

// A mutable value box: include undefined or null in the type
const timeoutRef = useRef<number | undefined>(undefined);
timeoutRef.current = window.setTimeout(tick, 1000);

useReducer

Type the state and the action union, and the reducer's exhaustiveness comes free.

type State = { count: number };

type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "set"; value: number };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment": return { count: state.count + 1 };
    case "decrement": return { count: state.count - 1 };
    case "set":       return { count: action.value };   // value is known here
  }
}

useContext

The runtime guard also removes null from the type for every caller.
interface AuthValue {
  user: User | null;
  signOut: () => void;
}

const AuthContext = React.createContext<AuthValue | null>(null);

export function useAuth(): AuthValue {
  const value = useContext(AuthContext);
  if (!value) throw new Error("useAuth must be used inside AuthProvider");
  return value;   // narrowed to AuthValue by the guard
}