| name | typescript-pro |
| description | ๐ท Write advanced TypeScript with strict mode, generics, branded types, and type-safe patterns. Activate for TS architecture, type errors, tsconfig tuning, JS-to-TS migration, or building type-safe APIs and libraries. |
๐ท TypeScript Pro
Leverage the full power of TypeScript's type system to catch bugs at compile time, not runtime. Treat types as documentation that the compiler enforces -- every any is a missed opportunity.
Core Principles
- Enable strict mode always. Set
strict: true in tsconfig.json. Treat noUncheckedIndexedAccess, exactOptionalPropertyTypes, and verbatimModuleSyntax as non-negotiable in new projects.
- Eliminate
any. Use unknown for truly unknown data, then narrow with type guards. Reserve any only for legacy interop boundaries.
- Encode invariants in types. If a value cannot be negative, make the type prevent it. If a function requires a non-empty array, express that in the signature.
- Prefer inference over annotation. Let TypeScript infer return types and variable types. Annotate function parameters and public API boundaries explicitly.
- Keep type utilities small and composable. Build complex types from simple building blocks, not monolithic conditional chains.
Workflow
- Audit the tsconfig. Confirm
strict: true and evaluate additional flags like noUncheckedIndexedAccess.
- Model the domain. Define branded types, discriminated unions, and enums before writing logic.
- Write the contract. Define function signatures, generics, and overloads. Let the types guide the implementation.
- Implement with narrowing. Use
in, typeof, discriminant checks, and exhaustive switches -- avoid casts.
- Validate at boundaries. Parse external data with Zod, io-ts, or manual type guards at API/IO edges.
- Refactor with the compiler. Rename, restructure, and tighten types -- let red squiggles reveal every call site that needs updating.
Examples
Branded Types for Domain Safety
function getUser(id: string): User { ... }
getUser(orderId);
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
const toUserId = (id: string): UserId => id as UserId;
function getUser(id: UserId): User { ... }
getUser(orderId);
Exhaustive Discriminated Unions
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "rect": return s.width * s.height;
default: return s satisfies never;
}
}
Common Patterns
Constrained Generics with Defaults
function merge<T extends Record<string, unknown>>(base: T, override: Partial<T>): T {
return { ...base, ...override };
}
Mapped Types for Form State
type FormErrors<T> = { [K in keyof T]?: string };
type FormTouched<T> = { [K in keyof T]?: boolean };
Template Literal Types for Routes
type Method = "GET" | "POST" | "PUT" | "DELETE";
type Route = `/api/${string}`;
type Endpoint = `${Method} ${Route}`;
Conditional Type Extraction
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnwrapPromise<Promise<string>>;
Const Assertions for Literal Inference
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number];
Anti-Patterns
- Casting instead of narrowing.
value as MyType silences the compiler without safety. Use type guards or satisfies instead.
- Exporting
any from library boundaries. Downstream consumers lose all type safety. Export precise types or unknown.
- Overusing enums. Prefer
as const objects or union literals -- they are more tree-shakable and interoperate better with plain JS.
- Giant conditional types. If a type spans 20+ lines, break it into named helpers. Types should be readable too.
- Ignoring
strictNullChecks. Optional chaining hides bugs when nullability is not tracked. Keep strict null checks on and handle every | undefined.
- Using
Object, Function, or {}. These are almost never what you want. Use Record<string, unknown>, (...args: unknown[]) => unknown, or a specific interface.