๐ŸŽ“ A1Learner
๐Ÿ”
0 of 37 lessons completed

TypeScript Branded Types

A branded type is a plain type intersected with a phantom member that marks its kind. The mark is the literal __brand member, and it exists only at compile time.

Plain strings swap by accident

Both ids are plain strings, so the user loader accepts a product id without complaint. The program compiles and prints a user page for a product.

function loadUser(id: string): string {
  return 'user page for ' + id;
}
const productId: string = 'p-9';
console.log(loadUser(productId));

Brands make the two ids different types

Each id type intersects string with its own __brand literal. The maker function checks the prefix and returns the branded type.

type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };
function makeUserId(raw: string): UserId {
  if (!raw.startsWith('u-')) {
    throw new Error('user id must start with u-');
  }
  return raw as UserId;
}
function loadUser(id: UserId): string {
  return 'user page for ' + id;
}
const userId = makeUserId('u-17');
console.log(loadUser(userId));
console.log(typeof userId);

The wrong brand is refused

The product id carries the ProductId brand, and the loader expects the UserId brand. The compiler compares the __brand literals and refuses the call.

type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };
function makeProductId(raw: string): ProductId {
  return raw as ProductId;
}
function loadUser(id: UserId): string {
  return 'user ' + id;
}
const productId = makeProductId('p-9');
console.log(loadUser(productId));

Example

type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };
function makeUserId(raw: string): UserId {
  return raw as UserId;
}
function makeProductId(raw: string): ProductId {
  return raw as ProductId;
}
function checkout(user: UserId, product: ProductId, quantity: number): string {
  return user + ' buys ' + quantity + ' x ' + product;
}
console.log(checkout(makeUserId('u-17'), makeProductId('p-9'), 2));

Key points

  • A brand adds a member named __brand whose type is a string literal naming the kind.
  • Two branded string types with different __brand literals are not interchangeable.
  • Maker functions validate a raw value and return the branded type.
  • Plain values cannot enter a branded position without passing a maker function.
  • The brand adds no runtime data, so a branded string still behaves as a string.

In this example, checkout takes one parameter of each branded type, so the two ids cannot trade places at the call. The maker functions are the only way in, and each one stamps exactly one brand.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท TypeScript Playground

Exercise

What does the __brand member add to a branded string at runtime?