Back to Blog
web-dev NyxaLabs Team •
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.
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"); // WorksDiscriminated 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