Branding nominal types
| Structural collisions | |
| Brand with a tag | |
| Use branded ids | |
| Escape hatch |
Structural collisions
TypeScript compares object shapes, not names, so two string ids look identical to the checker. A user id can flow into an order id slot with no warning at all.
The bug surfaces far from its cause: a mistyped argument fetches the wrong record. Branding gives each id a distinct type while keeping strings at runtime.
function loadOrder(id: string): string {
return "order:" + id;
}
const userId = "user-7";
loadOrder(userId);
Brand with a tag
A brand is an extra phantom field intersected with a plain type. It exists only for checking, never at runtime, yet it makes two string aliases incompatible.
Name the brand after the concept it protects. Clear brand names turn idle strings into documented domain values across the whole file.
type UserId = string & { readonly brand: "UserId" };
type OrderId = string & { readonly brand: "OrderId" };
Use branded ids
Constructors validate raw text once and return the branded result. After that point every function can demand the exact brand and reject lookalike strings.
Push validation to the edges: parse at input, then enjoy precise types inside. The middle layers stay short because wrong ids no longer compile.
function makeUserId(raw: string): UserId {
if (raw.length === 0) {
throw new Error("Empty user id");
}
return raw as UserId;
}
function greetUser(id: UserId): string {
return "hi " + id;
}
Escape hatch
Sometimes you must drop back to a plain string, for example to build a URL or write a log line. A tiny helper converts a branded id to its raw text in one visible step.
Keep escapes rare and explicit at boundaries. If raw text leaks everywhere, the brand loses value and mixups return through the back door.
function rawId(id: UserId): string {
return id as string;
}
console.log(rawId(makeUserId("user-7")));
Next: Result types
Article author: Arthur Isaev