Deck 12 - Advanced TypeScript for React (Senior TS Patterns) (objective)
Goal: write and review React+TS like a senior: sound types, safe boundaries, expressive component APIs, and correct narrowing.
You learn: what/why, common traps, and what to recommend during code review.
Output style: PASS/PARTIAL/FAIL + 2-3 issues + fix approach.
Mental model: TypeScript is structural, not nominal (what this means).
Types are compatible based on shape, not name. If two types have the same structure, they are assignable.
Why it matters: accidental compatibility can happen; use branded types when you need nominal-like safety.
Mental model: TS checks at compile time, not runtime.
Types disappear at runtime. If data comes from outside (API, localStorage), you must validate at runtime.
Best practice: parse/validate at boundaries (Zod/io-ts) before using data in React state.
Mental model: ‘unknown’ vs ‘any’ (how to choose).
unknown is safe: you must narrow before using. any disables type checking.
Senior rule: prefer unknown at boundaries; avoid any except in narrow interoperability seams.
Mental model: variance and why callbacks bite.
Function parameter types are (effectively) contravariant/bivariant in some React typings.
Why: you can accidentally accept too-broad/too-narrow handlers. Prefer explicit handler types and generic props when needed.
Mental model: widening vs literal types.
const x = ‘a’ preserves literal ‘a’; let x = ‘a’ widens to string.
Why: literal types enable discriminated unions and safer component APIs.
Mental model: inference is your friend, but be explicit at boundaries.
Let TS infer locals; be explicit at component boundaries (props, return values of hooks, public APIs).
Why: keeps code readable and prevents accidental breaking changes.
Mental model: type-level code has cost (cognitive + compile time).
Overly clever conditional types can slow TS and confuse reviewers.
Best practice: prefer simple types unless complexity buys real safety.
Mental model: narrowing is driven by control flow.
TS narrows based on checks like typeof, in, instanceof, and discriminants.
Senior: structure code to make narrowing obvious and local.
Mental model: ‘satisfies’ keeps literals while checking shape.
Use ‘satisfies’ to ensure an object meets a type without widening the object’s inferred literal types.
Great for configuration maps and component prop dictionaries.
Mental model: generics are about relationships.
Generic types express ‘this depends on that’. Example: a function that returns the same element type it receives.
Senior: use generics to model relationships, not to ‘avoid writing types’.
Mental model: discriminated unions model state machines.
Use a shared literal field (status/type) to represent distinct states.
Why: prevents impossible states and improves React UI branching.
Mental model: never trust API response shapes.
Even if backend is ‘typed’, responses can change, be partial, or error.
Fix: validate + handle missing fields; model error paths explicitly.
Mental model: avoid ‘boolean soup’ in props.
Multiple booleans create invalid combinations and unclear meaning.
Prefer enums/unions (variant: ‘primary’|’secondary’) or discriminated props.
Mental model: prefer readonly for inputs.
Mark props and shared objects readonly to prevent mutation bugs.
Why: mutation breaks memoization and surprises consumers.
Mental model: null vs undefined (be consistent).
Pick a consistent meaning: undefined often means ‘not provided’; null often means ‘known empty’.
Senior: be consistent in React state and API models.
Mental model: Exhaustiveness is a correctness feature.
Use exhaustive checks in switch on unions to catch missing cases at compile time.
Why: prevents runtime bugs when new variants are added.
Mental model: prefer domain types over primitives (when it helps).
Instead of string for everything, use types like UserId, Email.
Why: reduces accidental mixing of IDs/values.
Mental model: React types can lie if you force them.
Casting (as) can silence errors but create runtime bugs.
Senior: only cast after validating, and keep casts narrow and well-justified.
Mental model: build type safety from the edges inward.
Start with typed API client + runtime validation, then typed hooks, then typed components.
Why: prevents ‘any leaks’ from contaminating your app.
Best practice: type props with an explicit Props type.
Define Props separately and give components a single named prop type.
Why: improves readability, reusability, and keeps component signatures stable.
Best practice: do NOT use React.FC by default (why).
React.FC adds implicit children and can complicate generics/defaultProps.
Prefer: function Component(props: Props) or const Component = (props: Props) => …
Best practice: type state as a union that matches reality.
Example: User | null, or {status:’loading’} | {status:’success’, data:User}.
Why: prevents impossible states and makes render branching safe.
Best practice: use discriminated union for async state.
Represent async state with status: ‘idle’|’loading’|’error’|’success’.
Why: TS can narrow state so UI code is correct by construction.