TypeScript has one of the strongest type systems among mainstream languages. Beyond basic type annotations, it gives you tools to model complex data with compile-time safety. Here are a few patterns that go past everyday TypeScript usage.
Generic Constraints
Generics become truly powerful when combined with constraints. By narrowing what a generic type can be, you unlock autocompletion and catch errors that would otherwise slip through.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
const name = getProperty(user, "name"); // type: string
const age = getProperty(user, "age"); // type: number
// getProperty(user, "address"); // Error: not assignable to "name" | "age" | "email"Conditional Types
Conditional types allow you to create types that depend on other types, similar to ternary expressions but at the type level. They are the foundation of many advanced patterns.
type ApiResponse<T> = T extends Array<infer U>
? { data: U[]; total: number }
: { data: T };
type UserListResponse = ApiResponse<User[]>;
// { data: User[]; total: number }
type SingleUserResponse = ApiResponse<User>;
// { data: User }Discriminated Unions
Discriminated unions combine literal types with union types to create type-safe state machines. They are essential for modeling states that carry different data.
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function handleState(state: RequestState<User>) {
switch (state.status) {
case "success":
console.log(state.data.name); // TypeScript knows data exists
break;
case "error":
console.error(state.error.message); // TypeScript knows error exists
break;
}
}Template Literal Types
Template literal types let you construct string types from other types, enabling patterns like type-safe event emitters and route definitions.
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
type CSSProperty = "margin" | "padding";
type Direction = "top" | "right" | "bottom" | "left";
type CSSSpacing = `${CSSProperty}-${Direction}`;
// "margin-top" | "margin-right" | ... | "padding-left"These patterns work together to catch whole classes of bugs at compile time. Learning them takes some effort, but it pays off as your codebase grows. Refactors become safer, APIs document themselves, and many runtime errors turn into compile-time errors.