Most developers use TypeScript as "JavaScript with type annotations." But TypeScript's type system is a full-fledged programming language in itself. These advanced patterns will transform how you model your domains and catch entire categories of bugs at compile time.

Advanced TypeScript turns your type system into a powerful domain modeling tool
Discriminated Unions for State Machines
Instead of using booleans and optional fields to represent state, use discriminated unions. The compiler then guarantees you handle every possible state:
type ApiState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function renderUsers(state: ApiState<User[]>) {
switch (state.status) {
case "idle": return "Click to load";
case "loading": return "Loading...";
case "success": return state.data.map(u => u.name);
case "error": return state.error.message;
// TypeScript errors if you miss a case!
}
}This pattern eliminates impossible states. You can never accidentally access data during a loading state or forget to handle an error state.
Branded Types to Prevent Primitive Obsession
A common source of bugs is passing the wrong string or number to a function. Branded types add nominal typing to structural types:
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
function createUserId(id: string): UserId {
return id as UserId;
}
function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }
const userId = createUserId("u-123");
const orderId = createOrderId("o-456");
getUser(userId); // OK
getUser(orderId); // Compile error! OrderId is not UserIdThis catches a class of bugs that unit tests rarely cover — accidentally passing an order ID where a user ID is expected.

Branded types catch ID-swapping bugs that unit tests rarely cover
Conditional Types for Flexible APIs
Conditional types let you create utility types that adapt based on their input:
type ApiResponse<T> = T extends Array<infer Item>
? { items: Item[]; total: number }
: { data: T };
// ApiResponse<User[]> = { items: User[]; total: number }
// ApiResponse<User> = { data: User }The infer keyword is particularly powerful — it lets you extract types from complex generic structures at the type level.
Key Takeaways
- Discriminated unions eliminate impossible states and force exhaustive handling
- Branded types add nominal typing to prevent ID and value mix-ups
- Conditional types create flexible, context-aware type utilities
- Template literal types enable type-safe string manipulation (event names, routes, CSS)
The best TypeScript code reads like a specification of your domain. If someone can understand your types, they understand your system. Invest in your type definitions as much as your runtime code.