Back to all posts

Advanced TypeScript Patterns

TypeScriptMarch 5, 202410 min read
TypeScriptBest Practices

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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.