# TypeScript Branded Types: Preventing ID Mixups and Domain Modeling Errors

> How to use TypeScript branded types to prevent passing a UserId where an OrderId is expected — a practical guide to nominal typing in TypeScript.

- **Published:** 2026-01-28
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** typescript, branded-types, domain-modeling, type-safety, advanced
- **Canonical URL:** https://clixo.sh/blog/typescript-branded-types-domain-modeling

TypeScript uses structural typing: if two types have the same shape, they are interchangeable. For most cases this is what you want. For domain identifiers, it is a liability.

A `UserId` and an `OrderId` are both strings. TypeScript cannot tell them apart. Nothing stops you from passing a `UserId` to a function that expects an `OrderId` — and that bug does not crash immediately. It creates a database query against the wrong table, or returns data for the wrong entity, or silently does nothing. The kind of bug that lives in production for weeks.

Branded types are the solution.

## The Problem: Structural Typing with Identifiers

```typescript
type UserId = string;
type OrderId = string;

function getOrder(id: OrderId): Promise<Order> {
  return db.orders.findById(id);
}

const userId: UserId = "user-abc123";
getOrder(userId); // TypeScript accepts this — both are string
```

At the type level, `UserId` and `OrderId` are identical. TypeScript has no reason to reject the call. The developer made a mistake and the type system could not catch it.

## TypeScript Branded Types: The Pattern

A branded type uses an intersection with a phantom property to make two otherwise identical types structurally distinct:

```typescript
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type ProductId = Brand<string, "ProductId">;
```

The `__brand` property only exists at the type level — it is never set at runtime. The intersection creates a type that is structurally distinct from `string` and from other branded strings, while remaining assignable where `string` is expected.

```typescript
function getOrder(id: OrderId): Promise<Order> {
  return db.orders.findById(id);
}

const userId = "user-abc123" as UserId;
const orderId = "order-xyz789" as OrderId;

getOrder(orderId); // correct
getOrder(userId);  // Error: Argument of type 'UserId' is not assignable to parameter of type 'OrderId'
```

The mistake is now a compile error. The `__brand` type-level property distinguishes `UserId` from `OrderId` even though both are strings at runtime.

## Constructor Functions for Branded Types

The `as` assertion creates a branded value, but scattering assertions throughout the codebase defeats the purpose. Centralize creation in constructor functions:

```typescript
function makeUserId(raw: string): UserId {
  if (!raw.startsWith("user-")) {
    throw new Error(`Invalid UserId format: ${raw}`);
  }
  return raw as UserId;
}

function makeOrderId(raw: string): OrderId {
  if (!raw.startsWith("order-")) {
    throw new Error(`Invalid OrderId format: ${raw}`);
  }
  return raw as OrderId;
}
```

Now `as UserId` appears exactly once, inside `makeUserId`. The rest of the codebase creates `UserId` values through the constructor, which validates the format. The brand assertion is contained and auditable.

```mermaid
flowchart LR
  RAW["raw string"] --> CTOR["makeUserId()"]
  CTOR -->|"format valid"| UID["UserId branded"]
  CTOR -->|"format invalid"| ERR["throws Error"]
  UID --> FN["getUser(id: UserId)"]
  OID["OrderId branded"] -->|"compile error"| FN
```

Callers:

```typescript
const userId = makeUserId(user.id);   // UserId — validated and branded
const orderId = makeOrderId(order.id); // OrderId — validated and branded

getOrder(orderId); // correct
getOrder(userId);  // compile error
```

## Applying Branded Types to Database Query Results

The other common application is preventing unvalidated raw data from being treated as domain types. Mark values as validated when they come through your validation layer:

```typescript
type RawInput = string;
type ValidatedEmail = Brand<string, "ValidatedEmail">;

function validateEmail(raw: RawInput): ValidatedEmail {
  const email = raw.trim().toLowerCase();
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    throw new Error(`Invalid email: ${raw}`);
  }
  return email as ValidatedEmail;
}

function sendEmail(to: ValidatedEmail, subject: string): void {
  // guaranteed: to has been through email validation
}

const raw = req.body.email; // string — unvalidated
sendEmail(raw, "Welcome");  // Error: Argument of type 'string' is not assignable to parameter of type 'ValidatedEmail'

const validated = validateEmail(raw); // ValidatedEmail — validated
sendEmail(validated, "Welcome");      // correct
```

The function signature of `sendEmail` now documents and enforces a precondition. Code review and the type system together prevent calling `sendEmail` with unvalidated input.

## Branded Types vs Opaque Types: The Tradeoff

The brand pattern above uses `as` inside constructor functions. A stricter alternative uses a module with a private interface, sometimes called an opaque type. The tradeoff is that the brand pattern is simpler and works within a single module, while true opaque types require careful module boundaries.

For most applications, the brand pattern provides meaningful safety at low complexity cost. The `as` assertion is contained in one place per type, the type system catches mixups everywhere else, and the runtime overhead is zero.

## When to Use Branded Types

Use them for:

- Entity identifiers (UserId, OrderId, ProductId, SessionId)
- Validated strings (ValidatedEmail, SafeHtml, ParsedUrl)
- Units where mixing would be a bug (Dollars vs Cents, Meters vs Feet)
- Sanitized inputs (SqlSafe, HtmlEscaped)

Skip them for:

- Simple data fields where mixing is not a real risk
- Types that are only used in one function
- Cases where the nominal distinction is not actually enforced at the type level in practice

---

Domain modeling with branded types is one of those investments that pays off consistently as a codebase grows. If your team is building a TypeScript application with complex domain logic, [Clixo](https://clixo.sh/#contact) can help design a type architecture that prevents the class of bugs that audits and tests rarely catch.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
