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
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
}