Branded types cheatsheet.

Why brand?

TS is structural — same shape = same type:

type UserId = string;
type PostId = string;

function getUser(id: UserId) {}
const pid: PostId = "abc";
getUser(pid);                // no error — UserId and PostId are aliases for string

Branding makes them distinct.

Basic brand

type Brand<T, B> = T & { __brand: B };

type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;

const u = "abc" as UserId;
const p = "xyz" as PostId;

function getUser(id: UserId) {}
getUser(u);                  // ok
getUser(p);                  // ERROR
getUser("abc");              // ERROR (must brand)

Smart constructor

function userId(s: string): UserId {
  if (!s.startsWith("u_")) throw new Error("bad id");
  return s as UserId;
}

const u = userId("u_abc");

Validates at runtime, brands at compile.

Unique symbol brand (stronger)

declare const brand: unique symbol;
type Brand<T, B> = T & { [brand]: B };

Can’t be faked from outside the module.

Common branded types

type Email = Brand<string, "Email">;
type Url = Brand<string, "Url">;
type Iso8601 = Brand<string, "Iso8601">;
type PositiveInt = Brand<number, "PositiveInt">;
type Percent = Brand<number, "Percent">;

function email(s: string): Email {
  if (!s.includes("@")) throw new Error("not email");
  return s as Email;
}

function percent(n: number): Percent {
  if (n < 0 || n > 100) throw new Error("out of range");
  return n as Percent;
}

Branding number

type Cents = Brand<number, "Cents">;
type Dollars = Brand<number, "Dollars">;

function dollarsToCents(d: Dollars): Cents {
  return Math.round(d * 100) as Cents;
}

let cents = 100 as Cents;
let dollars = 1 as Dollars;
cents = dollars;             // ERROR

Use to prevent unit confusion (dollars/cents, meters/feet, ms/s).

Brand + literal types

type Status = Brand<"active" | "inactive", "Status">;

function status(s: string): Status {
  if (s !== "active" && s !== "inactive") throw new Error("bad status");
  return s as Status;
}

Result<Branded, Error>

type Result<T> = { ok: true; value: T } | { ok: false; error: string };

function tryEmail(s: string): Result<Email> {
  if (!s.includes("@")) return { ok: false, error: "no @" };
  return { ok: true, value: s as Email };
}

Cleaner than throwing for invalid input.

Zod + brand

import { z } from "zod";

const Email = z.string().email().brand<"Email">();
type Email = z.infer<typeof Email>;

const e = Email.parse("[email protected]");   // Email

Zod’s .brand() produces a TS brand from the schema.

Type-only newtype (no runtime cost)

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

type UserId = Branded<string, "UserId">;

// At runtime, UserId is just a string.
JSON.stringify({ id: "abc" as UserId });    // {"id":"abc"}

No __brand property exists; it’s purely structural cast hack.

Asserting brands

function assertEmail(s: string): asserts s is Email {
  if (!s.includes("@")) throw new Error("not email");
}

const x: string = "...";
assertEmail(x);
x;                     // narrowed to Email

Stripping brand

function unbrand<T>(x: Brand<T, any>): T {
  return x as T;
}

const raw: string = unbrand(emailValue);

Useful at the serialization boundary.

When to brand

  • IDs that should not be mixed (UserId vs OrgId).
  • Validated strings (Email, Url).
  • Units (Cents, Dollars, Meters, Feet).
  • Bounded numbers (Percent, PositiveInt, Probability).
  • Trusted strings (SafeHtml after sanitization).

When not to

  • Plain config strings.
  • One-off values inside a single module.
  • When the validation is trivial (e.g. just typeof === "string").

Branding has a maintenance cost.

Common mistakes

  • Branding everything — type-checker noise outweighs safety.
  • Forgetting the smart constructor — brand without validation is meaningless.
  • Brand without readonly — TS may surface it as a writable field.
  • Mixing brand styles in the same project.

Read this next

If you want my branded-types + Zod library, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .