Typing React: Props, Hooks, Context & Generics
📖 Concept
React and TypeScript are a natural fit — TypeScript catches prop misuse, hook errors, and context issues at compile time. Modern React (function components + hooks) is where TypeScript shines.
Component typing patterns:
React.FC<Props>— Functional component type (debated — many teams avoid it)- Direct annotation:
function MyComponent(props: Props): JSX.Element React.PropsWithChildren<Props>— Addchildrento your props
Hook typing:
useState<T>()— Usually inferred, but specify for complex/union stateuseRef<T>(null)— The generic determines what the ref points touseReducer— Discriminated unions for actions → exhaustive type safety
Event typing:
React.ChangeEvent<HTMLInputElement>— Input change eventsReact.FormEvent<HTMLFormElement>— Form submissionReact.MouseEvent<HTMLButtonElement>— Click events
Generic components let you build reusable components where the data type flows through — like a <List<T>> that knows the type of each item.
🏠 Real-world analogy: TypeScript in React is like having a building inspector review your blueprints (props) before construction (rendering). Wrong wiring (prop types)? Caught before the walls go up, not after a fire.
💻 Code Example
1// Component with typed props2interface ButtonProps {3 label: string;4 variant?: "primary" | "secondary" | "danger";5 disabled?: boolean;6 onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;7}89function Button({ label, variant = "primary", disabled, onClick }: ButtonProps) {10 return (11 <button12 className={`btn btn-${variant}`}13 disabled={disabled}14 onClick={onClick}15 >16 {label}17 </button>18 );19}2021// Children typing22interface CardProps {23 title: string;24 children: React.ReactNode; // Accepts anything renderable25}2627function Card({ title, children }: CardProps) {28 return (29 <div className="card">30 <h2>{title}</h2>31 {children}32 </div>33 );34}3536// ⭐ useState with unions (discriminated union state)37type FormState =38 | { status: "idle" }39 | { status: "submitting" }40 | { status: "success"; data: { id: string } }41 | { status: "error"; error: string };4243function useFormSubmit() {44 const [state, setState] = React.useState<FormState>({ status: "idle" });4546 async function submit(data: FormData) {47 setState({ status: "submitting" });48 try {49 const result = await fetch("/api/submit", { method: "POST", body: data });50 const json = await result.json();51 setState({ status: "success", data: json });52 } catch (e) {53 setState({ status: "error", error: (e as Error).message });54 }55 }56 return { state, submit };57}5859// useRef typing60function TextInput() {61 const inputRef = React.useRef<HTMLInputElement>(null);6263 function focusInput() {64 inputRef.current?.focus(); // Optional chaining — ref might be null65 }6667 return <input ref={inputRef} />;68}6970// useReducer with discriminated union actions71type CounterAction =72 | { type: "increment" }73 | { type: "decrement" }74 | { type: "reset"; payload: number };7576function counterReducer(state: number, action: CounterAction): number {77 switch (action.type) {78 case "increment": return state + 1;79 case "decrement": return state - 1;80 case "reset": return action.payload;81 }82}8384// ⭐ Generic component85interface ListProps<T> {86 items: T[];87 renderItem: (item: T, index: number) => React.ReactNode;88 keyExtractor: (item: T) => string;89}9091function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {92 return (93 <ul>94 {items.map((item, i) => (95 <li key={keyExtractor(item)}>{renderItem(item, i)}</li>96 ))}97 </ul>98 );99}100101// Usage — T is inferred as User!102// <List103// items={users}104// renderItem={(user) => <span>{user.name}</span>}105// keyExtractor={(user) => user.id.toString()}106// />107108// Context with TypeScript109interface AuthContext {110 user: { id: string; name: string } | null;111 login: (email: string, password: string) => Promise<void>;112 logout: () => void;113}114115const AuthContext = React.createContext<AuthContext | null>(null);116117function useAuth(): AuthContext {118 const ctx = React.useContext(AuthContext);119 if (!ctx) throw new Error("useAuth must be used within AuthProvider");120 return ctx;121}
🏋️ Practice Exercise
Mini Exercise:
- Create a typed form component with
onChangehandlers for different input types - Build a generic
<Select<T>>component where options and selected value are typed - Use
useReducerwith a discriminated union of 5+ action types - Create a typed Context + custom hook pattern with proper null-safety
- Type a
useDebounce<T>hook that preserves the generic type of the debounced value
⚠️ Common Mistakes
Using
React.FCand relying on its implicitchildren— React 18 removed implicit children from FC; always declare children explicitly in propsTyping state as the initial value's type only —
useState('')givesstring, but if state can benull, useuseState<string | null>(null)Forgetting that
useRef<T>(null)starts as null — always use optional chainingref.current?.method()or check for nullCreating context without a null check —
createContext(undefined as any)is unsafe; use a custom hook that throws if context is nullOver-typing event handlers —
(e: React.ChangeEvent<HTMLInputElement>) => voidon every handler; often the inline type is inferred
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Typing React: Props, Hooks, Context & Generics