$ cat docs/react-+-typescript-patterns.md
updated Today · 22 min read · published
React + TypeScript Patterns Introduction
React and TypeScript together catch prop mismatches, bad event handlers, and hook misuse before runtime. This page is the practical pattern catalog — props, hooks, events, generics, and context.
Typing Props
Copy 1 type ButtonProps = { 2 label: string; 3 onClick: () => void; 4 variant?: "primary" | "ghost"; 5 disabled?: boolean; 6 }; 7 8 export function Button({ label, onClick, variant = "primary", disabled }: ButtonProps) { 9 return ( 10 <button data-variant={variant} disabled={disabled} onClick={onClick}> 11 {label} 12 </button> 13 ); 14 } 15 16 // Children 17 type CardProps = { 18 title: string; 19 children: React.ReactNode; 20 }; 21 22 // Prefer React.ReactNode over JSX.Element for flexibility
📝 note
Avoid React.FC unless you need its implicit children behavior historically — modern React prefers explicit props.
useState, useRef, useReducer
Copy 1 import { useState, useRef, useReducer } from "react"; 2 3 // Infer from initial value 4 const [count, setCount] = useState(0); // number 5 6 // Explicit when initial is null 7 const [user, setUser] = useState<User | null>(null); 8 9 const inputRef = useRef<HTMLInputElement>(null); 10 inputRef.current?.focus(); 11 12 type State = { status: "idle" | "loading" | "done"; data?: string }; 13 type Action = 14 | { type: "start" } 15 | { type: "success"; data: string } 16 | { type: "reset" }; 17 18 function reducer(state: State, action: Action): State { 19 switch (action.type) { 20 case "start": 21 return { status: "loading" }; 22 case "success": 23 return { status: "done", data: action.data }; 24 case "reset": 25 return { status: "idle" }; 26 default: { 27 const _e: never = action; 28 return state; 29 } 30 } 31 }
Events
Copy 1 import type { FormEvent, ChangeEvent, MouseEvent } from "react"; 2 3 function onSubmit(e: FormEvent<HTMLFormElement>) { 4 e.preventDefault(); 5 } 6 7 function onChange(e: ChangeEvent<HTMLInputElement>) { 8 console.log(e.target.value); 9 } 10 11 function onClick(e: MouseEvent<HTMLButtonElement>) { 12 console.log(e.currentTarget.name); 13 } 14 15 // Keyboard 16 function onKeyDown(e: React.KeyboardEvent<HTMLDivElement>) { 17 if (e.key === "Escape") e.currentTarget.blur(); 18 }
Element Event type input change ChangeEvent<HTMLInputElement> form submit FormEvent<HTMLFormElement> button click MouseEvent<HTMLButtonElement> generic handler prop (e: MouseEvent<HTMLButtonElement>) => void
Generic Components
generic-list.tsx wrap TypeScript
Copy 1 type ListProps<T> = { 2 items: T[]; 3 renderItem: (item: T) => React.ReactNode; 4 keyFn: (item: T) => string; 5 }; 6 7 export function List<T>({ items, renderItem, keyFn }: ListProps<T>) { 8 return ( 9 <ul> 10 {items.map((item) => ( 11 <li key={keyFn(item)}>{renderItem(item)}</li> 12 ))} 13 </ul> 14 ); 15 } 16 17 // Usage — T inferred 18 <List 19 items={users} 20 keyFn={(u) => u.id} 21 renderItem={(u) => u.name} 22 />;
Context Typing
context.tsx wrap TypeScript
Copy 1 import { createContext, useContext } from "react"; 2 3 type Auth = { 4 user: User | null; 5 login: (token: string) => void; 6 logout: () => void; 7 }; 8 9 const AuthContext = createContext<Auth | null>(null); 10 11 export function useAuth(): Auth { 12 const ctx = useContext(AuthContext); 13 if (!ctx) throw new Error("useAuth requires AuthProvider"); 14 return ctx; 15 }
Async UI & Data
Copy 1 type Query<T> = 2 | { status: "idle" } 3 | { status: "loading" } 4 | { status: "success"; data: T } 5 | { status: "error"; error: Error }; 6 7 function UserBadge({ query }: { query: Query<User> }) { 8 switch (query.status) { 9 case "idle": 10 case "loading": 11 return <span>Loading…</span>; 12 case "error": 13 return <span>{query.error.message}</span>; 14 case "success": 15 return <span>{query.data.name}</span>; 16 default: { 17 const _x: never = query; 18 return _x; 19 } 20 } 21 }
Extending Native Element Props
Copy 1 type InputProps = React.ComponentPropsWithoutRef<"input"> & { 2 label: string; 3 }; 4 5 export function Field({ label, id, ...rest }: InputProps) { 6 return ( 7 <label> 8 <span>{label}</span> 9 <input id={id} {...rest} /> 10 </label> 11 ); 12 } 13 14 // ComponentPropsWithRef when forwarding refs
React+TS Checklist
Practice Why Explicit prop types Documents the contract Discriminated UI state Exhaustive rendering Typed events Correct target fields Generic list/table Reuse without any Context null + hook guard Fail fast outside provider Validate form input Runtime + types aligned
More React + TS Patterns
Polymorphic as prop (simplified)
polymorphic.tsx wrap TypeScript
Copy 1 type ButtonOwnProps<E extends React.ElementType> = { 2 as?: E; 3 children: React.ReactNode; 4 }; 5 6 type ButtonProps<E extends React.ElementType> = ButtonOwnProps<E> & 7 Omit<React.ComponentPropsWithoutRef<E>, keyof ButtonOwnProps<E>>; 8 9 export function Button<E extends React.ElementType = "button">({ 10 as, 11 children, 12 ...rest 13 }: ButtonProps<E>) { 14 const Tag = as || "button"; 15 return <Tag {...rest}>{children}</Tag>; 16 }
forwardRef
forward-ref.tsx wrap TypeScript
Copy 1 import { forwardRef } from "react"; 2 3 type InputProps = React.ComponentPropsWithoutRef<"input"> & { label: string }; 4 5 export const Input = forwardRef<HTMLInputElement, InputProps>( 6 function Input({ label, id, ...rest }, ref) { 7 return ( 8 <label> 9 <span>{label}</span> 10 <input ref={ref} id={id} {...rest} /> 11 </label> 12 ); 13 }, 14 );
useEffectEvent-style stable handler (conceptual)
use-event.ts wrap TypeScript
Copy 1 // Keep effect subscriptions stable while reading latest props 2 function useEvent<T extends (...args: never[]) => unknown>(fn: T): T { 3 const ref = useRef(fn); 4 ref.current = fn; 5 return useCallback((...args: never[]) => ref.current(...args), []) as T; 6 }
Custom Hooks Typing
custom-hooks.ts wrap TypeScript
Copy 1 function useToggle(initial = false): [boolean, () => void] { 2 const [on, setOn] = useState(initial); 3 const toggle = useCallback(() => setOn((v) => !v), []); 4 return [on, toggle]; 5 } 6 7 function useFetch<T>(url: string): Query<T> { 8 const [state, setState] = useState<Query<T>>({ status: "loading" }); 9 useEffect(() => { 10 let cancelled = false; 11 fetch(url) 12 .then((r) => r.json()) 13 .then((data: unknown) => { 14 if (!cancelled) setState({ status: "success", data: data as T }); // prefer Zod 15 }) 16 .catch((error: unknown) => { 17 if (!cancelled) 18 setState({ 19 status: "error", 20 error: error instanceof Error ? error : new Error(String(error)), 21 }); 22 }); 23 return () => { 24 cancelled = true; 25 }; 26 }, [url]); 27 return state; 28 }
✓ best practice
Return tuples with const labels when helpful: [state, actions] as const patterns.
Children & Slots
Copy 1 type SlotProps = { 2 header?: React.ReactNode; 3 footer?: React.ReactNode; 4 children: React.ReactNode; 5 }; 6 7 export function Panel({ header, footer, children }: SlotProps) { 8 return ( 9 <section> 10 {header ? <header>{header}</header> : null} 11 <div>{children}</div> 12 {footer ? <footer>{footer}</footer> : null} 13 </section> 14 ); 15 } 16 17 // Function-as-children 18 type RenderProp<T> = { 19 value: T; 20 children: (value: T) => React.ReactNode; 21 }; 22 export function Show<T>({ value, children }: RenderProp<T>) { 23 return <>{children(value)}</>; 24 }
ComponentProps Helpers
untitled.typescript wrap TypeScript
Copy 1 type ButtonProps = React.ComponentPropsWithoutRef<"button">; 2 type ButtonRef = React.ElementRef<"button">; 3 type DivProps = React.ComponentProps<"div">; 4 5 // Extract props from a custom component 6 type Of<T> = T extends React.ComponentType<infer P> ? P : never; 7 type P = Of<typeof Button>;
Typing Async Boundaries
untitled.typescript wrap TypeScript
Copy 1 type ErrorBoundaryState = { error: Error | null }; 2 class ErrorBoundary extends React.Component< 3 { children: React.ReactNode; fallback: React.ReactNode }, 4 ErrorBoundaryState 5 > { 6 state: ErrorBoundaryState = { error: null }; 7 static getDerivedStateFromError(error: Error) { 8 return { error }; 9 } 10 render() { 11 if (this.state.error) return this.props.fallback; 12 return this.props.children; 13 } 14 }
Worked Examples — react-ts
End-to-end snippets for react-ts. Type these into a strict project and mutate them until the checker teaches you the edges.
Example A
untitled.typescript wrap TypeScript
Copy 1 type Id = string & { readonly __brand: unique symbol }; 2 function asId(s: string): Id { return s as Id; } 3 4 type Entity = { id: Id; createdAt: Date }; 5 function touch(e: Entity): Entity { 6 return { ...e, createdAt: new Date() }; 7 }
Example B — conditional distribute
untitled.typescript wrap TypeScript
Copy 1 type IsString<T> = T extends string ? true : false; 2 type A = IsString<"hi" | 1>; // true | false (distributed) 3 4 type IsStringBox<T> = [T] extends [string] ? true : false; 5 type B = IsStringBox<"hi" | 1>; // false
Example C — key remapping filter
untitled.typescript wrap TypeScript
Copy 1 type OnlyStrings<T> = { 2 [K in keyof T as T[K] extends string ? K : never]: T[K]; 3 }; 4 type User = { id: string; age: number; name: string }; 5 type Names = OnlyStrings<User>; // { id: string; name: string }
Example D — template route params
untitled.typescript wrap TypeScript
Copy 1 type ExtractParams<S extends string> = 2 S extends `${string}:${infer P}/${infer Rest}` 3 ? P | ExtractParams<Rest> 4 : S extends `${string}:${infer P}` 5 ? P 6 : never; 7 type Params = ExtractParams<"/users/:id/posts/:postId">; // "id" | "postId"
ℹ info
If an example fails in your TS version, check the handbook page for version notes — ForgeLearn targets modern TypeScript 5.x.
Self-Check Questions
Answer without looking. Then verify in the playground / tsc.
# Question 1 What is the difference between annotation, assertion, and satisfies? 2 When does a conditional type distribute over a union? 3 How do you prove exhaustiveness for a discriminated union? 4 Why is unknown safer than any at JSON boundaries? 5 What does noUncheckedIndexedAccess change about obj[key]? 6 How do you preserve literal types through a generic helper? 7 When should you prefer interface over type (and vice versa)? 8 How does z.infer keep runtime and types aligned?
✓ best practice
Agents: treat these as verification prompts — generate answers with code evidence, then PASS/FAIL.
Agent Rules for React+TS
Generate explicit prop types; type events with element generics; prefer discriminated union UI state; validate form values with Zod before setState.
Extended Drills (react) — Set 1
Extended reference material for deliberate practice. Work through each snippet under strict: true and note where inference surprises you.
Snippet 1 — deep-readonly Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.
deep-readonly-1.ts wrap TypeScript
Copy 1 type DeepReadonly<T> = { 2 readonly [K in keyof T]: T[K] extends (infer U)[] 3 ? readonly DeepReadonly<U>[] 4 : T[K] extends object 5 ? DeepReadonly<T[K]> 6 : T[K]; 7 };
Snippet 2 — uti Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.
Copy 1 type UnionToIntersection<U> = 2 (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; 3 type I = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a:1 } & { b:2 }
Snippet 3 — narrow-key Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.
narrow-key-3.ts wrap TypeScript
Copy 1 function narrowKey<T extends object, K extends keyof T>( 2 obj: T, 3 key: K, 4 guard: (v: unknown) => boolean, 5 ): obj is T & Record<K, T[K]> { 6 return guard(obj[key]); 7 }
Snippet 4 — as-tuple Practice snippet 4. Predict the resulting type, then confirm with hover types in your editor.
as-tuple-4.ts wrap TypeScript
Copy 1 const asTuple = <const T extends readonly unknown[]>(xs: T) => xs; 2 const t = asTuple([1, "a", true]);
Snippet 5 — json Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.
Copy 1 type Json = 2 | string 3 | number 4 | boolean 5 | null 6 | Json[] 7 | { [key: string]: Json };
Snippet 6 — entries Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.
entries-6.ts wrap TypeScript
Copy 1 type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T]; 2 type E = Entries<{ a: 1; b: string }>; // ["a", 1] | ["b", string]
Snippet 7 — atleast Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.
atleast-7.ts wrap TypeScript
Copy 1 type RequireAtLeastOne<T> = { 2 [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>; 3 }[keyof T]; 4 type Patches = RequireAtLeastOne<{ a?: number; b?: string }>;
Snippet 8 — mutable-keys Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.
mutable-keys-8.ts wrap TypeScript
Copy 1 type MutableKeys<T> = { 2 [K in keyof T]-?: Equal<{ -readonly [P in K]: T[K] }, { [P in K]: T[K] }> extends true ? K : never; 3 }[keyof T]; 4 type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
Snippet 9 — snake Practice snippet 9. Predict the resulting type, then confirm with hover types in your editor.
Copy 1 type Snake<S extends string> = S extends `${infer H}${infer T}` 2 ? T extends Uncapitalize<T> 3 ? `${Lowercase<H>}${Snake<T>}` 4 : `${Lowercase<H>}_${Snake<Uncapitalize<T>>}` 5 : S;
Snippet 10 — publicof Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.
publicof-10.ts wrap TypeScript
Copy 1 type PublicOf<T> = { [K in keyof T]: T[K] }; 2 class Svc { #secret = 1; public open = true; } 3 type P = PublicOf<Svc>;
📝 note
These snippets are intentionally dense. Prefer understanding one fully over skimming all.
Extended Drills (react) — Set 2
Extended reference material for deliberate practice. Work through each snippet under strict: true and note where inference surprises you.
Snippet 1 — deep-readonly Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.
deep-readonly-1.ts wrap TypeScript
Copy 1 type DeepReadonly<T> = { 2 readonly [K in keyof T]: T[K] extends (infer U)[] 3 ? readonly DeepReadonly<U>[] 4 : T[K] extends object 5 ? DeepReadonly<T[K]> 6 : T[K]; 7 };
Snippet 2 — uti Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.
Copy 1 type UnionToIntersection<U> = 2 (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; 3 type I = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a:1 } & { b:2 }
Snippet 3 — narrow-key Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.
narrow-key-3.ts wrap TypeScript
Copy 1 function narrowKey<T extends object, K extends keyof T>( 2 obj: T, 3 key: K, 4 guard: (v: unknown) => boolean, 5 ): obj is T & Record<K, T[K]> { 6 return guard(obj[key]); 7 }
Snippet 4 — as-tuple Practice snippet 4. Predict the resulting type, then confirm with hover types in your editor.
as-tuple-4.ts wrap TypeScript
Copy 1 const asTuple = <const T extends readonly unknown[]>(xs: T) => xs; 2 const t = asTuple([1, "a", true]);
Snippet 5 — json Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.
Copy 1 type Json = 2 | string 3 | number 4 | boolean 5 | null 6 | Json[] 7 | { [key: string]: Json };
Snippet 6 — entries Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.
entries-6.ts wrap TypeScript
Copy 1 type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T]; 2 type E = Entries<{ a: 1; b: string }>; // ["a", 1] | ["b", string]
Snippet 7 — atleast Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.
atleast-7.ts wrap TypeScript
Copy 1 type RequireAtLeastOne<T> = { 2 [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>; 3 }[keyof T]; 4 type Patches = RequireAtLeastOne<{ a?: number; b?: string }>;
Snippet 8 — mutable-keys Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.
mutable-keys-8.ts wrap TypeScript
Copy 1 type MutableKeys<T> = { 2 [K in keyof T]-?: Equal<{ -readonly [P in K]: T[K] }, { [P in K]: T[K] }> extends true ? K : never; 3 }[keyof T]; 4 type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
Snippet 9 — snake Practice snippet 9. Predict the resulting type, then confirm with hover types in your editor.
Copy 1 type Snake<S extends string> = S extends `${infer H}${infer T}` 2 ? T extends Uncapitalize<T> 3 ? `${Lowercase<H>}${Snake<T>}` 4 : `${Lowercase<H>}_${Snake<Uncapitalize<T>>}` 5 : S;
Snippet 10 — publicof Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.
publicof-10.ts wrap TypeScript
Copy 1 type PublicOf<T> = { [K in keyof T]: T[K] }; 2 class Svc { #secret = 1; public open = true; } 3 type P = PublicOf<Svc>;
📝 note
These snippets are intentionally dense. Prefer understanding one fully over skimming all.
$ Blueprint — Engineering Documentation · Section ID: TS-REACT · Revision: 1.0