Back to Blog
web-dev

Advanced TypeScript Patterns: Type-Safe API Clients and Branded Types

Master advanced TypeScript patterns including branded types, discriminated unions, and type-safe API clients that catch errors at compile time.

NyxaLabs Team
Advanced TypeScript Patterns: Type-Safe API Clients and Branded Types

TypeScript's type system is more powerful than most developers realize. These advanced patterns will help you write safer, more maintainable code.

Branded Types for Primitive Safety

Primitive obsession leads to bugs. Is that string a userId or an email? Branded types make primitives distinguishable:

type Brand<K, T> = K & { __brand: T };

type UserId = Brand<string, "UserId">;
type Email = Brand<string, "Email">;

function createUserId(id: string): UserId {
  return id as UserId;
}

function sendEmail(to: Email, subject: string) {
  // ...
}

const userId = createUserId("user_123");
const email = "test@example.com" as Email;

// sendEmail(userId, "Hello"); // Compile error!
sendEmail(email, "Hello"); // Works

Discriminated Unions for State Machines

type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

function handleState<T>(state: AsyncState<T>) {
  switch (state.status) {
    case "idle":
      return "Ready to fetch";
    case "loading":
      return "Loading...";
    case "success":
      return state.data; // TypeScript knows data exists
    case "error":
      return state.error.message; // TypeScript knows error exists
  }
}

Type-Safe API Client with Zod

import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.string().datetime(),
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return UserSchema.parse(data); // Runtime validation + type inference
}

Builder Pattern with Method Chaining

class QueryBuilder<T> {
  private filters: Record<string, unknown> = {};
  private sortField?: keyof T;
  private sortOrder: "asc" | "desc" = "asc";
  
  where<K extends keyof T>(field: K, value: T[K]): this {
    this.filters[field as string] = value;
    return this;
  }
  
  orderBy(field: keyof T, order: "asc" | "desc" = "asc"): this {
    this.sortField = field;
    this.sortOrder = order;
    return this;
  }
  
  build(): QueryConfig {
    return { filters: this.filters, sort: { field: this.sortField, order: this.sortOrder } };
  }
}

// Usage with full type safety
const query = new QueryBuilder<User>()
  .where("email", "test@example.com")
  .orderBy("createdAt", "desc")
  .build();

Template Literal Types for Routes

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = `/api/${string}`;

type RouteDefinition<M extends HttpMethod, R extends ApiRoute> = {
  method: M;
  route: R;
};

const routes = {
  getUser: { method: "GET", route: "/api/users/:id" },
  createUser: { method: "POST", route: "/api/users" },
} as const satisfies Record<string, RouteDefinition<HttpMethod, ApiRoute>>;

Conditional Types for API Responses

type ApiResponse<T> = T extends void
  ? { success: boolean }
  : { success: boolean; data: T };

function apiCall<T>(endpoint: string): Promise<ApiResponse<T>> {
  // Implementation
}

NyxaLabs TypeScript Expertise

We write production TypeScript that scales. Our code reviews and architecture consultations help teams level up their TypeScript skills. Reach out to improve your codebase quality.

Tags

#TypeScript #Type Safety #API Design #Best Practices #Tutorial

Want to Work With Us?

We're ready to help bring your ideas to life with cutting-edge technology.

Get In Touch