TypeScript compares types by shape, so two aliases of string are interchangeable. A branded type adds a tag that exists only in the type system, string & { readonly __brand: "UserId" }, which makes a UserId incompatible with a plain string and with every other brand:
At runtime userId is just the string "u_42". The brand is a compile-time label, and its only job is to stop IDs, units and validated strings from being mixed up.
The Problem: Aliases Are Only Names
A type alias does not create a new type. It gives an existing type a second name, and the compiler treats both names as the same thing:
This is structural typing: TypeScript checks that the shape fits, and string fits string. For objects the shapes usually differ; for IDs, emails, currencies and units, they never do. Brands fix that one case.
How the Brand Works
string & { readonly __brand: "UserId" } is an intersection: a value must be a string and also have a __brand property of type "UserId". No real string has that property, so no plain string is assignable to it:
index.ts(10,10): error TS2345: Argument of type 'string' is not assignable to parameter of type 'UserId'.
Type 'string' is not assignable to type '{ readonly __brand: "UserId"; }'.
index.ts(11,10): error TS2345: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'.
Type 'OrderId' is not assignable to type '{ readonly __brand: "UserId"; }'.
Types of property '__brand' are incompatible.
Type '"OrderId"' is not assignable to type '"UserId"'.
The direction that matters still works: a UserId is a string, so you can pass it to anything that takes a string, call .startsWith() on it, or put it in a template. The brand only blocks the way in.
Constructor Functions That Validate
The as UserId assertion is the only way in, and an assertion checks nothing. Put it in one function that validates the input, and every branded value in the program is then known to have passed that check:
sendWelcome never checks its input again, because its parameter type says the check already happened. This is the "parse, don't validate" idea: check at the boundary, then carry the proof in the type. A type guard works as a constructor too when you prefer a boolean to an exception: function isEmail(s: string): s is Email.
A Generic Brand Helper
Writing the intersection by hand for each type gets repetitive. A small generic does it once:
Branding a number works the same way as branding a string. Note that amount / 100 is a plain number: arithmetic on a branded number gives an unbranded result, covered below.
unique symbol Brands
A string property name like __brand looks like a real property: userId.__brand type-checks as "UserId" but is undefined at runtime, and two libraries could pick the same name. A unique symbol key avoids both:
declare const brand: unique symbol declares a symbol that exists only for the type checker; the declare keyword means no JavaScript is emitted for it. Because the symbol is not exported from its module, code in other files cannot even name the brand property, so outside that module the only ways to get a Meters are the functions you export or an as Meters assertion.
Brands Cost Nothing at Runtime
The compiled output contains no trace of the brand. These are the lines emitted for the Meters example, below the module header the compiler adds: the declare, both type aliases and every as are gone, and the suppressed call on the last line still runs.
function toMeters(feet) {
return (feet * 0.3048);
}
const height = 10;
const inMeters = toMeters(height);
console.log(inMeters.toFixed(3)); // 3.048
// @ts-expect-error: Meters is not Feet
toMeters(inMeters);
A branded value is the plain primitive: typeof gives "string" or "number", JSON.stringify writes it as usual, and comparisons work as before. The flip side is that nothing is checked at runtime unless your constructor function checks it. Data parsed from JSON, a database or a URL arrives as string, and it becomes a UserId only when you pass it through that function.
Arithmetic and Methods Drop the Brand
Operations on a branded value return the base type, because the brand is not part of what + or .slice() produce:
type Cents = number & { readonly __brand: "Cents" };
const a = 500 as Cents;
const b = 250 as Cents;
const sum = a + b; // number, not Cents
const total: Cents = a + b; // error TS2322: Type 'number' is not assignable to type 'Cents'
const fixed = (a + b) as Cents; // re-brand when the result is still valid
That is usually what you want: adding two amounts in cents gives cents, but multiplying cents by cents does not, and only you know which operations keep the meaning. Write small helpers such as addCents(a: Cents, b: Cents): Cents for the operations your code needs.
When to Use Branded Types
Use brands where mixing up two values of the same primitive type is a real risk and the compiler cannot otherwise help:
| Situation | Example brands |
|---|---|
| IDs from different tables | UserId, OrderId, ProductId |
| Validated strings | Email, Url, NonEmptyString, Slug |
| Units and currencies | Meters, Feet, Cents, Usd, Eur |
| Sanitized or escaped text | SafeHtml, SqlIdentifier |
| Numbers with a range | Percentage, PositiveInt |
Skip them for values that are never confused, and for object types that already differ in shape. Validation libraries can produce branded types from a schema: in Zod, z.string().brand<"UserId">() gives a schema whose parse returns a branded UserId, which saves writing the constructor functions by hand.
Frequently Asked Questions
What are branded types in TypeScript?
A pattern that makes two types with the same runtime representation incompatible. You intersect the base type with a tag that no ordinary value has: type UserId = string & { readonly __brand: "UserId" }. A plain string, or an OrderId with a different tag, is then rejected where a UserId is expected.
Does TypeScript have nominal types?
No. TypeScript's type system is structural: two types with the same shape are interchangeable, whatever their names. Class declarations with private or #private members behave nominally, and branded types are the common way to get the same effect for primitives like strings and numbers.
Do branded types have a runtime cost?
No. The brand exists only in the type. The value is still a plain string or number at runtime, with no extra property, and the compiled JavaScript is the same as without the brand. The only runtime code is whatever validation you choose to put in the function that creates branded values.
How do I create a value of a branded type?
With a type assertion, ideally in one small function that checks the input first: function toEmail(s: string): Email { if (!s.includes("@")) throw new Error("bad email"); return s as Email; }. Keeping the as in that one place means every Email in the program has passed the check.
What is the difference between a type alias and a branded type?
type UserId = string is only a new name: any string is accepted wherever a UserId is expected. type UserId = string & { readonly __brand: "UserId" } is a new, incompatible type: a plain string must go through a constructor function or an assertion first.