Claude-initial-setup type-patterns
git clone https://github.com/VersoXBT/claude-initial-setup
T=$(mktemp -d) && git clone --depth=1 https://github.com/VersoXBT/claude-initial-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/typescript/type-patterns" ~/.claude/skills/versoxbt-claude-initial-setup-type-patterns && rm -rf "$T"
skills/typescript/type-patterns/SKILL.mdTypeScript Type Patterns
Apply advanced type-level patterns to model domains precisely and catch logic errors at compile time instead of runtime.
When to Use
- Modeling state machines or multi-variant data
- Preventing invalid values (e.g., mixing IDs of different entities)
- Building type-safe string patterns or event systems
- Ensuring all union cases are handled in switch/if chains
- Creating immutable constant objects with full type inference
Core Patterns
Pattern 1: Discriminated Unions
Use a shared literal property to let TypeScript narrow union members automatically.
interface Loading { status: "loading" } interface Success<T> { status: "success"; data: T } interface Failure { status: "error"; error: Error } type AsyncState<T> = Loading | Success<T> | Failure; function render(state: AsyncState<string>): string { switch (state.status) { case "loading": return "Loading..."; case "success": return state.data; // narrowed to Success<string> case "error": return state.error.message; // narrowed to Failure } }
Pattern 2: Exhaustive Checking with never
Guarantee every union variant is handled. If a new variant is added, the compiler flags every switch that misses it.
function assertNever(value: never): never { throw new Error(`Unhandled value: ${JSON.stringify(value)}`); } type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number } | { kind: "triangle"; base: number; height: number }; function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rect": return shape.width * shape.height; case "triangle": return (shape.base * shape.height) / 2; default: return assertNever(shape); // compile error if a case is missing } }
Pattern 3: Branded Types
Prevent mixing structurally identical types by attaching a phantom brand.
type Brand<T, B extends string> = T & { readonly __brand: B }; type UserId = Brand<string, "UserId">; type OrderId = Brand<string, "OrderId">; function createUserId(id: string): UserId { return id as UserId; } function createOrderId(id: string): OrderId { return id as OrderId; } function getUser(id: UserId): void { /* ... */ } const userId = createUserId("u-123"); const orderId = createOrderId("o-456"); getUser(userId); // OK // getUser(orderId); // Compile error -- OrderId is not UserId
Pattern 4: Template Literal Types
Build type-safe string patterns that the compiler validates.
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; type ApiVersion = "v1" | "v2"; type Resource = "users" | "orders" | "products"; type ApiRoute = `/${ApiVersion}/${Resource}`; // "/v1/users" | "/v1/orders" | "/v1/products" | "/v2/users" | ... type EventName<T extends string> = `on${Capitalize<T>}`; type ClickEvent = EventName<"click">; // "onClick" // Infer parts from template literals type ExtractResource<T> = T extends `/${string}/${infer R}` ? R : never; type Route = ExtractResource<"/v1/users">; // "users"
Pattern 5: Const Assertions and Readonly Tuples
Use
as const to preserve literal types and create fully immutable structures.
// Without as const: type is { method: string; url: string } // With as const: type preserves exact literals const config = { method: "GET", url: "/api/users", headers: ["Content-Type", "Authorization"], } as const; // config.method is "GET", not string // config.headers is readonly ["Content-Type", "Authorization"] // Derive union types from const objects const STATUS_CODES = { OK: 200, NOT_FOUND: 404, SERVER_ERROR: 500, } as const; type StatusCode = (typeof STATUS_CODES)[keyof typeof STATUS_CODES]; // 200 | 404 | 500 // Derive from const arrays const ROLES = ["admin", "editor", "viewer"] as const; type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
Pattern 6: Type Guards
Create reusable runtime checks that inform the compiler.
interface ApiError { code: number; message: string; } function isApiError(value: unknown): value is ApiError { return ( typeof value === "object" && value !== null && "code" in value && "message" in value && typeof (value as ApiError).code === "number" && typeof (value as ApiError).message === "string" ); } async function fetchData(url: string): Promise<string> { const response = await fetch(url); const body: unknown = await response.json(); if (isApiError(body)) { throw new Error(`API error ${body.code}: ${body.message}`); } return body as string; }
Anti-Patterns
-
String enums without discrimination -- Use discriminated unions instead of plain string enums when variants carry different data. Enums alone cannot narrow.
-
Overusing
for branded types -- Limitas
to brand constructors only. Every other usage should rely on narrowing.as -
Forgetting the
-- Without the exhaustive check, new union members silently fall through withdefault: assertNever
behavior.undefined -
on mutable variables --as const
makes the value deeply readonly. Do not assign it to a mutable variable and expect mutation to work.as const
Quick Reference
| Pattern | Use Case |
|---|---|
| Discriminated unions | Multi-state data, state machines |
| Exhaustive check (never) | Guarantee all cases handled |
| Branded types | Prevent ID/value mixing |
| Template literal types | Type-safe string patterns |
| const assertions | Immutable configs, derive unions |
| Type guards | Runtime validation with type narrowing |