TypeScript discussions often devolve into advanced type-level programming that’s impressive but rarely practical. Let’s focus on the patterns that make a real difference in day-to-day code.
Discriminated Unions
This is the single most useful TypeScript pattern. Model your states explicitly:
type ApiResult<T> =
| { status: "loading" }
| { status: "error"; error: Error }
| { status: "success"; data: T };
function handleResult(result: ApiResult<User>) {
switch (result.status) {
case "loading":
return showSpinner();
case "error":
return showError(result.error); // error is narrowed
case "success":
return showUser(result.data); // data is narrowed
}
}
The compiler ensures you handle every case. Add a new variant and TypeScript will flag every switch statement that doesn’t account for it.
The satisfies Operator
Introduced in TypeScript 4.9, satisfies lets you validate a value against a type while keeping its narrower inferred type:
const config = {
port: 3000,
host: "localhost",
debug: true,
} satisfies Record<string, string | number | boolean>;
// config.port is `number`, not `string | number | boolean`
This is especially useful for configuration objects and lookup tables where you want both validation and precise types.
Branded Types
Prevent accidental value swaps with branded types:
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
function getUser(id: UserId): User { /* ... */ }
const userId = "abc-123" as UserId;
const orderId = "xyz-789" as OrderId;
getUser(userId); // OK
getUser(orderId); // Type error
This costs nothing at runtime but prevents an entire class of bugs where IDs get mixed up.
const Assertions for Literals
When you need TypeScript to infer literal types rather than widened ones:
const routes = {
home: "/",
blog: "/blog",
about: "/about",
} as const;
// typeof routes.home is "/" not string
type Route = (typeof routes)[keyof typeof routes];
// Route is "/" | "/blog" | "/about"
Record + Extract for Type-Safe Maps
Build type-safe handler maps:
type EventType = "click" | "hover" | "submit";
type EventHandlers = Record<EventType, (event: Event) => void>;
const handlers: EventHandlers = {
click: (e) => console.log("clicked"),
hover: (e) => console.log("hovered"),
submit: (e) => console.log("submitted"),
};
Miss a handler and the compiler tells you. These patterns aren’t clever tricks — they’re practical tools that catch real bugs. Reach for them before reaching for complex conditional types.