A practical guide to the type-level patterns used throughout this library. Each section explains when to use the pattern, how it works, and provides copy-paste examples.
1. Branded Types (Nominal Typing)
7. Mapped Types
10. Decision Tree: Which Pattern to Use
Problem: TypeScript uses structural typing, so string is string regardless of what it represents. You can accidentally pass a userId where an orderId is expected.
Solution: Add a phantom property that exists only in the type system.
// Define the brand
declare const __brand: unique symbol;
type Brand<Base, Tag extends string> = Base & { readonly [__brand]: Tag };
// Create domain-specific types
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
// This prevents mixing them up:
function getUser(id: UserId): void { /* ... */ }
const orderId = "ord_123" as OrderId;
getUser(orderId); // Compile error!
const userId = "usr_456" as UserId;
getUser(userId); // Works!When to use: Domain IDs, validated strings (emails, URLs), constrained numbers (percentages, positive integers).
When NOT to use: Simple strings that don't represent distinct concepts. Don't over-brand — it adds ceremony without benefit if you're just passing generic strings around.
See: src/branded.ts for ready-to-use branded types and validators.
Problem: You have multiple related types and need to handle each case differently.
Solution: Add a literal type or _tag property that TypeScript can use for narrowing.
// The _tag field is the discriminant
type Shape =
| { _tag: "circle"; radius: number }
| { _tag: "rectangle"; width: number; height: number }
| { _tag: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape._tag) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height; // TypeScript knows width/height exist
case "triangle":
return (shape.base * shape.height) / 2;
}
}Exhaustiveness checking: TypeScript ensures you handle all cases:
function describe(shape: Shape): string {
switch (shape._tag) {
case "circle":
return "round";
case "rectangle":
return "boxy";
// Forgot "triangle"!
default:
// This line causes a compile error because `shape` is `Triangle`, not `never`
const _exhaustive: never = shape;
return _exhaustive;
}
}See: src/result.ts uses this pattern for Result (Ok | Err) and Option (Some | None).
Problem: You receive unknown data (API response, user input) and need to safely access typed properties.
Solution: Write functions with value is Type return types.
// Basic guard
function isString(value: unknown): value is string {
return typeof value === "string";
}
// Structural guard (validates object shape)
interface User {
id: number;
name: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
typeof (value as Record<string, unknown>).id === "number" &&
typeof (value as Record<string, unknown>).name === "string" &&
typeof (value as Record<string, unknown>).email === "string"
);
}
// Usage: TypeScript narrows the type inside the if-block
function processInput(data: unknown) {
if (isUser(data)) {
console.log(data.name); // TypeScript knows data is User
}
}Composable guards (from src/guards.ts):
import { hasShape, isString, isNumber, isArrayOf } from "./src/guards";
// Build complex guards from simple ones
const isUser = (v: unknown): v is User =>
hasShape(v, {
id: isNumber,
name: isString,
email: isString,
});
// Guard for arrays of a specific type
const isUserList = (v: unknown): v is User[] => isArrayOf(v, isUser);See: src/guards.ts for a complete set of primitive, structural, and composable guards.
Problem: You want a function to work with many types, but not *all* types — only those that meet certain criteria.
Solution: Use extends to constrain type parameters.
// Only accepts objects with an `id` property
function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find((item) => item.id === id);
}
// Only accepts string keys (not symbols or numbers)
function pick<T extends Record<string, unknown>, K extends keyof T>(
obj: T,
...keys: K[]
): Pick<T, K> {
// ...
}
// Constraining to specific shapes
function merge<
T extends Record<string, unknown>,
U extends Record<string, unknown>,
>(a: T, b: U): T & U {
return { ...a, ...b };
}Key insight: extends in generics means "must be assignable to", not "must inherit from". Think of it as a constraint, not inheritance.
Problem: A type should change based on the input type.
Solution: Use T extends X ? Y : Z syntax.
// Extract the element type from an array, or keep the type as-is
type Unpack<T> = T extends (infer U)[] ? U : T;
type A = Unpack<string[]>; // string
type B = Unpack<number>; // number
// Remove null/undefined from a type
type NonNullable<T> = T extends null | undefined ? never : T;
// Extract the return type of a function
type ReturnOf<T> = T extends (...args: unknown[]) => infer R ? R : never;
type C = ReturnOf<() => string>; // string
type D = ReturnOf<(x: number) => boolean>; // booleanThe infer keyword lets you "capture" a type variable inside the conditional:
// Extract the resolved type from a Promise
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type E = Awaited<Promise<Promise<string>>>; // string (recursively unwraps)Problem: You want to type-check string patterns at compile time.
Solution: Use TypeScript's template literal type syntax.
// Event name pattern
type EventName = `${string}:${string}`;
function on(event: EventName, handler: Function): void { /* ... */ }
on("user:login", handler); // OK
on("click", handler); // Error: doesn't match pattern
// Generate types from unions
type Color = "red" | "blue" | "green";
type Shade = "light" | "dark";
type ColorVariant = `${Shade}-${Color}`;
// "light-red" | "light-blue" | "light-green" | "dark-red" | "dark-blue" | "dark-green"
// CSS-like units
type CSSLength = `${number}${"px" | "em" | "rem" | "%"}`;
const valid: CSSLength = "16px"; // OK
const invalid: CSSLength = "16pt"; // ErrorProblem: You want to create a new type by transforming every property of an existing type.
Solution: Use { [K in keyof T]: ... } syntax.
// Make all properties optional
type Partial<T> = { [K in keyof T]?: T[K] };
// Make all properties readonly
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Make all properties nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Transform property types
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }Key modifier: Use + and - to add or remove modifiers:
// Remove readonly from all properties
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// Remove optional from all properties
type Required<T> = { [K in keyof T]-?: T[K] };type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;type RequireAtLeastOne<T> = {
[K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>;
}[keyof T];
// Usage
interface Filters {
name?: string;
email?: string;
role?: string;
}
type ValidFilter = RequireAtLeastOne<Filters>;
// Must provide at least one of name, email, or role// Built-in Omit doesn't error if you omit a key that doesn't exist
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type User = { name: string; email: string; age: number };
type A = StrictOmit<User, "name">; // OK
type B = StrictOmit<User, "invalid">; // Error! 'invalid' not in keyof Usertype PathsOf<T, Prefix extends string = ""> = T extends Record<string, unknown>
? {
[K in keyof T & string]: T[K] extends Record<string, unknown>
? PathsOf<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string]
: never;
interface Config {
server: { host: string; port: number };
database: { url: string };
}
type ConfigPaths = PathsOf<Config>;
// "server.host" | "server.port" | "database.url"Object.keys() returns string[], not (keyof T)[]const obj = { a: 1, b: 2, c: 3 };
// WRONG: TypeScript infers key as string
Object.keys(obj).forEach((key) => {
obj[key]; // Error: no index signature
});
// CORRECT: Cast the keys
(Object.keys(obj) as (keyof typeof obj)[]).forEach((key) => {
obj[key]; // Works!
});
// BETTER: Use Object.entries which gives you both key and value
Object.entries(obj).forEach(([key, value]) => {
console.log(key, value); // key is string, value is number
});// WRONG: TypeScript infers string[], not the literal union
const statuses = ["active", "inactive", "pending"];
// Type: string[]
// CORRECT: Use as const
const statuses = ["active", "inactive", "pending"] as const;
// Type: readonly ["active", "inactive", "pending"]
// Element type: "active" | "inactive" | "pending"interface Config { host: string; port: number }
// Object literal — excess properties caught
const config: Config = { host: "localhost", port: 3000, extra: true }; // Error!
// Variable assignment — excess properties NOT caught
const raw = { host: "localhost", port: 3000, extra: true };
const config: Config = raw; // No error! TypeScript allows this.unknown vs any// any: disables all type checking (dangerous)
const x: any = fetchData();
x.nonexistent.method(); // No error, crashes at runtime
// unknown: forces you to check before using (safe)
const y: unknown = fetchData();
y.nonexistent; // Error! Must narrow first
if (typeof y === "object" && y !== null && "name" in y) {
y.name; // OK, TypeScript narrowed the type
}Use this to pick the right pattern for your situation:
Is the problem about...
├── Preventing mix-ups between same-shaped types?
│ └── Use BRANDED TYPES (src/branded.ts)
│
├── Handling multiple related variants?
│ └── Use DISCRIMINATED UNIONS (Result, Option, or custom)
│
├── Validating data from external sources?
│ ├── At compile time? → Use GENERIC CONSTRAINTS
│ └── At runtime? → Use TYPE GUARDS (src/guards.ts)
│
├── Making errors explicit in function signatures?
│ └── Use RESULT TYPE (src/result.ts)
│
├── Representing optional values?
│ ├── Simple null check? → Just use T | null
│ └── Need chaining/mapping? → Use OPTION TYPE (src/result.ts)
│
├── Transforming object types?
│ ├── All properties? → Use MAPPED TYPES
│ └── Based on conditions? → Use CONDITIONAL TYPES
│
├── Type-safe string patterns?
│ └── Use TEMPLATE LITERAL TYPES
│
└── Complex object construction?
└── Use BUILDER PATTERN (src/patterns/index.ts)
Part of Frontend Developer Pro