CodePark/ questions / TypeScript on the server
START PRACTICINGPRACTICE
Backend question · TypeScript › TypeScript on the server

getOrder(userId) compiles even though a user ID is not an order ID, because both are strings. How do branded types prevent this?

type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

const toUserId = (s: string) => s as UserId;
declare function getOrder(id: OrderId): void;

const uid = toUserId("u_1");
getOrder(uid);     // error
getOrder("o_1");   // error: plain string

A branded type adds a fake marker to the base type, so UserId and OrderId no longer match. TypeScript is structural: it compares types by their shape, not their name. So two aliases of string are interchangeable, and passing a user ID where an order ID is expected compiles.

A branded type intersects the base type with a phantom property: a property that never exists at runtime. This makes UserId and OrderId incompatible with each other and with plain strings.

Values get the brand only through a small constructor or validator that uses as. Ideally this happens at the boundary, where the ID is parsed or read from the database. After that, mixing IDs is a compile error. The value is still a plain string for JSON, SQL drivers and logging.

The brand exists only at the type level. So it adds no runtime cost and no runtime check. Zod can produce branded types with .brand().

  • Structural typing makes aliases of string interchangeable.
  • A brand is the base type intersected with a phantom property.
  • Create branded values in one place, such as a constructor or validator.
  • A brand has no runtime cost, and the value is still a plain string at runtime.