|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/react
$cat docs/react-+-typescript-patterns.md
updated Today·22 min read·published

React + TypeScript Patterns

TypeScriptReactPatternsIntermediate🎯Free Tools
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
props.tsx
TypeScript
1type ButtonProps = {
2 label: string;
3 onClick: () => void;
4 variant?: "primary" | "ghost";
5 disabled?: boolean;
6};
7
8export 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
17type 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
hooks.ts
TypeScript
1import { useState, useRef, useReducer } from "react";
2
3// Infer from initial value
4const [count, setCount] = useState(0); // number
5
6// Explicit when initial is null
7const [user, setUser] = useState<User | null>(null);
8
9const inputRef = useRef<HTMLInputElement>(null);
10inputRef.current?.focus();
11
12type State = { status: "idle" | "loading" | "done"; data?: string };
13type Action =
14 | { type: "start" }
15 | { type: "success"; data: string }
16 | { type: "reset" };
17
18function 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
events.tsx
TypeScript
1import type { FormEvent, ChangeEvent, MouseEvent } from "react";
2
3function onSubmit(e: FormEvent<HTMLFormElement>) {
4 e.preventDefault();
5}
6
7function onChange(e: ChangeEvent<HTMLInputElement>) {
8 console.log(e.target.value);
9}
10
11function onClick(e: MouseEvent<HTMLButtonElement>) {
12 console.log(e.currentTarget.name);
13}
14
15// Keyboard
16function onKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
17 if (e.key === "Escape") e.currentTarget.blur();
18}
ElementEvent type
input changeChangeEvent<HTMLInputElement>
form submitFormEvent<HTMLFormElement>
button clickMouseEvent<HTMLButtonElement>
generic handler prop(e: MouseEvent<HTMLButtonElement>) => void
Generic Components
generic-list.tsx
TypeScript
1type ListProps<T> = {
2 items: T[];
3 renderItem: (item: T) => React.ReactNode;
4 keyFn: (item: T) => string;
5};
6
7export 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
TypeScript
1import { createContext, useContext } from "react";
2
3type Auth = {
4 user: User | null;
5 login: (token: string) => void;
6 logout: () => void;
7};
8
9const AuthContext = createContext<Auth | null>(null);
10
11export function useAuth(): Auth {
12 const ctx = useContext(AuthContext);
13 if (!ctx) throw new Error("useAuth requires AuthProvider");
14 return ctx;
15}
Async UI & Data
async.tsx
TypeScript
1type Query<T> =
2 | { status: "idle" }
3 | { status: "loading" }
4 | { status: "success"; data: T }
5 | { status: "error"; error: Error };
6
7function 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
attrs.tsx
TypeScript
1type InputProps = React.ComponentPropsWithoutRef<"input"> & {
2 label: string;
3};
4
5export 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
Forms with Zod
forms.ts
TypeScript
1const Schema = z.object({
2 email: z.string().email(),
3 age: z.coerce.number().int().min(13),
4});
5type FormValues = z.infer<typeof Schema>;
6
7function onSubmit(raw: unknown) {
8 const parsed = Schema.safeParse(raw);
9 if (!parsed.success) return parsed.error.flatten();
10 submit(parsed.data);
11}
React+TS Checklist
PracticeWhy
Explicit prop typesDocuments the contract
Discriminated UI stateExhaustive rendering
Typed eventsCorrect target fields
Generic list/tableReuse without any
Context null + hook guardFail fast outside provider
Validate form inputRuntime + types aligned
More React + TS Patterns

Polymorphic as prop (simplified)

polymorphic.tsx
TypeScript
1type ButtonOwnProps<E extends React.ElementType> = {
2 as?: E;
3 children: React.ReactNode;
4};
5
6type ButtonProps<E extends React.ElementType> = ButtonOwnProps<E> &
7 Omit<React.ComponentPropsWithoutRef<E>, keyof ButtonOwnProps<E>>;
8
9export 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
TypeScript
1import { forwardRef } from "react";
2
3type InputProps = React.ComponentPropsWithoutRef<"input"> & { label: string };
4
5export 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
TypeScript
1// Keep effect subscriptions stable while reading latest props
2function 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
TypeScript
1function useToggle(initial = false): [boolean, () => void] {
2 const [on, setOn] = useState(initial);
3 const toggle = useCallback(() => setOn((v) => !v), []);
4 return [on, toggle];
5}
6
7function 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
slots.tsx
TypeScript
1type SlotProps = {
2 header?: React.ReactNode;
3 footer?: React.ReactNode;
4 children: React.ReactNode;
5};
6
7export 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
18type RenderProp<T> = {
19 value: T;
20 children: (value: T) => React.ReactNode;
21};
22export function Show<T>({ value, children }: RenderProp<T>) {
23 return <>{children(value)}</>;
24}
ComponentProps Helpers
untitled.typescript
TypeScript
1type ButtonProps = React.ComponentPropsWithoutRef<"button">;
2type ButtonRef = React.ElementRef<"button">;
3type DivProps = React.ComponentProps<"div">;
4
5// Extract props from a custom component
6type Of<T> = T extends React.ComponentType<infer P> ? P : never;
7type P = Of<typeof Button>;
Typing Async Boundaries
untitled.typescript
TypeScript
1type ErrorBoundaryState = { error: Error | null };
2class 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
TypeScript
1type Id = string & { readonly __brand: unique symbol };
2function asId(s: string): Id { return s as Id; }
3
4type Entity = { id: Id; createdAt: Date };
5function touch(e: Entity): Entity {
6 return { ...e, createdAt: new Date() };
7}

Example B — conditional distribute

untitled.typescript
TypeScript
1type IsString<T> = T extends string ? true : false;
2type A = IsString<"hi" | 1>; // true | false (distributed)
3
4type IsStringBox<T> = [T] extends [string] ? true : false;
5type B = IsStringBox<"hi" | 1>; // false

Example C — key remapping filter

untitled.typescript
TypeScript
1type OnlyStrings<T> = {
2 [K in keyof T as T[K] extends string ? K : never]: T[K];
3};
4type User = { id: string; age: number; name: string };
5type Names = OnlyStrings<User>; // { id: string; name: string }

Example D — template route params

untitled.typescript
TypeScript
1type 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;
7type 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
1What is the difference between annotation, assertion, and satisfies?
2When does a conditional type distribute over a union?
3How do you prove exhaustiveness for a discriminated union?
4Why is unknown safer than any at JSON boundaries?
5What does noUncheckedIndexedAccess change about obj[key]?
6How do you preserve literal types through a generic helper?
7When should you prefer interface over type (and vice versa)?
8How 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
TypeScript
1type 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.

uti-2.ts
TypeScript
1type UnionToIntersection<U> =
2 (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
3type 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
TypeScript
1function 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
TypeScript
1const asTuple = <const T extends readonly unknown[]>(xs: T) => xs;
2const t = asTuple([1, "a", true]);

Snippet 5 — json

Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.

json-5.ts
TypeScript
1type 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
TypeScript
1type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T];
2type 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
TypeScript
1type RequireAtLeastOne<T> = {
2 [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
3}[keyof T];
4type 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
TypeScript
1type 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];
4type 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.

snake-9.ts
TypeScript
1type 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
TypeScript
1type PublicOf<T> = { [K in keyof T]: T[K] };
2class Svc { #secret = 1; public open = true; }
3type 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
TypeScript
1type 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.

uti-2.ts
TypeScript
1type UnionToIntersection<U> =
2 (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
3type 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
TypeScript
1function 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
TypeScript
1const asTuple = <const T extends readonly unknown[]>(xs: T) => xs;
2const t = asTuple([1, "a", true]);

Snippet 5 — json

Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.

json-5.ts
TypeScript
1type 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
TypeScript
1type Entries<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T];
2type 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
TypeScript
1type RequireAtLeastOne<T> = {
2 [K in keyof T]-?: Required<Pick<T, K>> & Partial<Omit<T, K>>;
3}[keyof T];
4type 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
TypeScript
1type 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];
4type 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.

snake-9.ts
TypeScript
1type 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
TypeScript
1type PublicOf<T> = { [K in keyof T]: T[K] };
2class Svc { #secret = 1; public open = true; }
3type 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

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.