TypeScript Patterns
Use this skill when changing TypeScript code and you need strong, practical defaults instead of a catalog of language features.
Goals
- Make invalid states hard to represent
- Keep inference helpful, not magical
- Validate runtime boundaries
- Avoid type gymnastics that reduce readability
Core Rules
1. Prefer inference until the contract matters
- Let TypeScript infer obvious locals.
- Add explicit types for public APIs, exported functions, complex return types, and shared constants.
- If inference produces a weak or widened type, annotate deliberately.
2. Avoid any
- Prefer
unknown at boundaries, then narrow it.
- If you must use
any, keep it local, document the reason, and do not let it leak into shared types.
3. Model state with discriminated unions
Use tagged unions for async state, UI state, workflow state, and domain outcomes.
Prefer:
{ status: 'idle' | 'loading' | 'success' | 'error' }
- tagged result objects with distinct shapes
Avoid:
- multiple loosely related booleans
- objects where every property is optional
4. Separate transport types from domain types
- DTOs from APIs, forms, and storage should not automatically become domain models.
- Convert at the boundary.
- Keep mapping logic explicit when names, nullability, or semantics differ.
5. Validate runtime input
- TypeScript checks compile-time assumptions, not runtime truth.
- Parse and validate untrusted data from:
- HTTP responses
- request bodies
- environment variables
- local storage
- message queues
- Prefer schema validation if the repo already uses it.
6. Use narrow utility types, not type puzzles
Pick, Omit, Partial, Readonly, and simple mapped types are fine.
- Avoid deeply nested conditional types unless they clearly pay for themselves.
- If a type takes longer to understand than the runtime code it protects, simplify it.
7. Prefer string unions over enums by default
- Use string literal unions for most application state and protocol tags.
- Use enums only when the repo already standardizes on them or interop requires them.
8. Use assertions sparingly
- Avoid
as unless you know something the compiler cannot.
- Never use assertions to suppress a real typing problem.
- Prefer narrowing, parsing, helper guards, or better function signatures.
Error and Result Modeling
- Prefer explicit result shapes or domain-specific errors at service boundaries.
- Do not throw raw strings.
- If using exceptions, throw
Error subclasses or a well-known error type.
- If using result objects, keep success and failure shapes distinct and easy to narrow.
React / UI Rules
Apply when the repo includes React or similar TSX-based UI:
- Prefer plain function components over
React.FC unless the repo already standardizes on React.FC.
- Keep prop types local to the component unless shared elsewhere.
- Use discriminated unions or small state machines for UI state.
- Type event handlers explicitly when inference is weak.
- Avoid enormous prop types built from many intersected utility types.
Node / Service Rules
Apply when the repo includes backend TypeScript:
- Type request/response boundaries explicitly.
- Parse config and env vars into a typed config object near startup.
- Keep shared error and result contracts small and stable.
- Do not let framework types bleed everywhere; wrap or narrow when needed.
tsconfig Expectations
Prefer these when you control the config or when reviewing a TS repo:
strict: true
noImplicitOverride: true
noUncheckedIndexedAccess: true when the repo can sustain it
exactOptionalPropertyTypes: true when the team is ready for the stricter contract
Do not expand the compiler surface casually in a small task; align with the repo.
Review Heuristics
Look for:
any creeping into shared code
- unsafe assertions
- DTOs passed deep into domain/UI without normalization
- optional fields used as hidden state machines
- impossible branches that should be encoded in types
- missing runtime validation at external boundaries
- utility types that obscure the real contract
Anti-Patterns
Avoid:
- exporting giant global types barrels without need
- reusing one interface for API input, API output, DB row, and UI state
- boolean soup for async/UI state
- assertion chains like
foo as Bar as Baz
- type-level cleverness that future maintainers will fear touching
Quick Checklist
1---2name: typescript-patterns3description: Practical TypeScript rules for safe application code, APIs, and UI state.4license: See repository LICENSE5---67# TypeScript Patterns89Use this skill when changing TypeScript code and you need strong, practical defaults instead of a catalog of language features.1011## Goals12131. Make invalid states hard to represent142. Keep inference helpful, not magical153. Validate runtime boundaries164. Avoid type gymnastics that reduce readability1718## Core Rules1920### 1. Prefer inference until the contract matters2122- Let TypeScript infer obvious locals.23- Add explicit types for public APIs, exported functions, complex return types, and shared constants.24- If inference produces a weak or widened type, annotate deliberately.2526### 2. Avoid `any`2728- Prefer `unknown` at boundaries, then narrow it.29- If you must use `any`, keep it local, document the reason, and do not let it leak into shared types.3031### 3. Model state with discriminated unions3233Use tagged unions for async state, UI state, workflow state, and domain outcomes.3435Prefer:3637- `{ status: 'idle' | 'loading' | 'success' | 'error' }`38- tagged result objects with distinct shapes3940Avoid:4142- multiple loosely related booleans43- objects where every property is optional4445### 4. Separate transport types from domain types4647- DTOs from APIs, forms, and storage should not automatically become domain models.48- Convert at the boundary.49- Keep mapping logic explicit when names, nullability, or semantics differ.5051### 5. Validate runtime input5253- TypeScript checks compile-time assumptions, not runtime truth.54- Parse and validate untrusted data from:55 - HTTP responses56 - request bodies57 - environment variables58 - local storage59 - message queues60- Prefer schema validation if the repo already uses it.6162### 6. Use narrow utility types, not type puzzles6364- `Pick`, `Omit`, `Partial`, `Readonly`, and simple mapped types are fine.65- Avoid deeply nested conditional types unless they clearly pay for themselves.66- If a type takes longer to understand than the runtime code it protects, simplify it.6768### 7. Prefer string unions over enums by default6970- Use string literal unions for most application state and protocol tags.71- Use enums only when the repo already standardizes on them or interop requires them.7273### 8. Use assertions sparingly7475- Avoid `as` unless you know something the compiler cannot.76- Never use assertions to suppress a real typing problem.77- Prefer narrowing, parsing, helper guards, or better function signatures.7879## Error and Result Modeling8081- Prefer explicit result shapes or domain-specific errors at service boundaries.82- Do not throw raw strings.83- If using exceptions, throw `Error` subclasses or a well-known error type.84- If using result objects, keep success and failure shapes distinct and easy to narrow.8586## React / UI Rules8788Apply when the repo includes React or similar TSX-based UI:8990- Prefer plain function components over `React.FC` unless the repo already standardizes on `React.FC`.91- Keep prop types local to the component unless shared elsewhere.92- Use discriminated unions or small state machines for UI state.93- Type event handlers explicitly when inference is weak.94- Avoid enormous prop types built from many intersected utility types.9596## Node / Service Rules9798Apply when the repo includes backend TypeScript:99100- Type request/response boundaries explicitly.101- Parse config and env vars into a typed config object near startup.102- Keep shared error and result contracts small and stable.103- Do not let framework types bleed everywhere; wrap or narrow when needed.104105## tsconfig Expectations106107Prefer these when you control the config or when reviewing a TS repo:108109- `strict: true`110- `noImplicitOverride: true`111- `noUncheckedIndexedAccess: true` when the repo can sustain it112- `exactOptionalPropertyTypes: true` when the team is ready for the stricter contract113114Do not expand the compiler surface casually in a small task; align with the repo.115116## Review Heuristics117118Look for:119120- `any` creeping into shared code121- unsafe assertions122- DTOs passed deep into domain/UI without normalization123- optional fields used as hidden state machines124- impossible branches that should be encoded in types125- missing runtime validation at external boundaries126- utility types that obscure the real contract127128## Anti-Patterns129130Avoid:131132- exporting giant global types barrels without need133- reusing one interface for API input, API output, DB row, and UI state134- boolean soup for async/UI state135- assertion chains like `foo as Bar as Baz`136- type-level cleverness that future maintainers will fear touching137138## Quick Checklist139140- [ ] Public contracts are explicit141- [ ] Runtime boundaries are validated142- [ ] `any` is absent or tightly contained143- [ ] State is modeled with clear tagged shapes144- [ ] Transport and domain types are not casually mixed145- [ ] Assertions are justified146- [ ] Types improve readability instead of reducing it