HomeBlogTypeScript
TypeScript

Designing TypeScript APIs That Don't Lie

Techniques for crafting TypeScript interfaces that accurately model your domain, prevent runtime errors at compile time, and make impossible states literally unrepresentable.

Jan 30, 2025 7 min read 15.8k views
TypeScript API Design DX Type Safety

TypeScript's type system is expressive enough to encode business rules directly into your types — if you use it correctly. Most TypeScript codebases settle for basic type annotations when they could be eliminating entire classes of bugs at the type level.

Make Impossible States Unrepresentable

The most powerful TypeScript technique is designing types so that invalid states simply cannot be expressed. If your code compiles, the invalid state cannot occur — no defensive checks needed at runtime.

typescript
// Bad: both fields can be undefined simultaneously
type Result = { data?: User; error?: string };

// Good: exactly one of these exists
type Result =
  | { status: 'success'; data: User }
  | { status: 'error'; message: string }
  | { status: 'loading' };

Branded Types for Domain Safety

Primitive obsession — using string or number for everything — is a silent killer. A userId and an orderId are both strings, but they should never be interchangeable. Branded types enforce this at the type level with zero runtime cost.

typescript
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

function getUser(id: UserId): Promise<User> { ... }

// TypeScript error: Argument of type 'OrderId' is
// not assignable to parameter of type 'UserId'
getUser(orderId);
💡 Tip

Pair branded types with a small factory function (e.g. asUserId(raw: string): UserId) at your system boundaries — API responses, database queries — to keep the casting contained.

Template Literal Types for String Validation

TypeScript's template literal types let you encode string shape constraints directly in the type system. You can enforce that an API route starts with '/', that a CSS variable starts with '--', or that an event name follows a 'namespace:event' pattern — all at compile time.

typescript
type Route = `/${string}`;
type CSSVar = `--${string}`;
type EventName = `${string}:${string}`;

// ✅ OK
const route: Route = '/dashboard';
// ❌ Error: Type '"dashboard"' is not assignable to type '`/${string}`'
const bad: Route = 'dashboard';

Found this useful?

Share it with your network