Branded Types in TypeScript: techniques

In this post, I’ll walk you through several ways to implement branded types in TypeScript. If you’re new to the concept, check out this intro to branded types before diving in.

Branded types let you create more specific types from primitives like string or number. This helps TypeScript catch bugs where you might accidentally mix up values that are technically the same primitive type but represent different concepts (e.g., UserId vs. string).

1. Using a Type Wrapper

type UserId = string & { __brand: "UserId" };

const createUserId = (raw: string): UserId => {
  // Add validation logic here if needed
  return raw as UserId;
};

const userId = createUserId("user_id_42");

function getUser(id: UserId) {
  /* ... */
}

getUser(userId); // ✅ Works
getUser("user_id_42"); // ❌ Type error

This pattern is lightweight and doesn’t require any extra dependencies. You define a branded type and enforce its usage via a factory function (createUserId). This ensures only properly branded values are passed around your codebase.

Pros:

Cons:

2. Using JavaScript Classes / Objects

class UserId {
  constructor(private _id: string) {}

  get id() {
    return this._id;
  }
}

const userId = new UserId("user_id_42");

function getUser(id: UserId) {
  /* ... */
}

getUser(userId); // ✅ Works
getUser("user_id_42"); // ❌ Type error

If you want to encapsulate logic (like validation, formatting, or transformation), classes are a good fit. For simple cases, though, this approach can be a bit verbose.

Pros:

Cons:

3. Using Zod’s brand() Method

import { z } from "zod";

const userIdSchema = z.string().startsWith("user_id").brand("UserId");

export const createUserId = (raw: string) => userIdSchema.parse(raw);

export type UserId = z.infer<typeof userIdSchema>;

function getUser(id: UserId) {
  /* ... */
}

const userId = createUserId("user_id_42");
getUser(userId); // ✅ Works
getUser("user_id_42"); // ❌ Type error

If you’re already using Zod, its brand method gives you branded types with built-in validation. This is especially handy for APIs or form inputs.

Pros:

Cons:

Summary

Branded types are a powerful way to make your TypeScript code safer and more expressive. Pick the approach that fits your project and team best! We in Saleor use both Zod brand and classes.