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
Get the full TypeScript Utility Library and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.