Branded types
Stop passing a product ID where a user ID goes, or dollars where cents go: brand a type, make values in one checked function, and know a brand's limits.
- 01Brand a typeWrite
string & { readonly __brand: "UserId" }or aBrand<T, Name>helper, so look-alike values stop fitting each other. - 02Make branded values safelyWrite one constructor function or type guard that checks a value and gives back the branded type.
- 03Know the limitsExplain why a brand is gone when the code runs, why
asskips every check, and why math gives back a plain number.
Two IDs that TypeScript keeps apart
A shop has users and products, and both have IDs. A user ID looks like "u-1" and a product ID looks like "p-7". Both are strings, so TypeScript can't tell them apart, and passing one where the other belongs is an easy mistake. This lesson gives each kind of ID a brand, so TypeScript can:
type UserId = string & { readonly __brand: "UserId" };type ProductId = string & { readonly __brand: "ProductId" }; function loadUser(id: UserId) { return `Loading user ${id}`;} const userId = "u-1" as UserId;const productId = "p-7" as ProductId; console.log(loadUser(userId));console.log(loadUser(productId));Line 1 makes a type called UserId. Read it as "a string that also carries the brand "UserId"". Line 2 does the same for ProductId. Lines 4 to 6 are a function that loads a user, so it asks for a UserId. Lines 8 and 9 make one ID of each kind. The as in "u-1" as UserId says "treat this text as a UserId"; a later section explains it.
Line 11 passes the user ID and prints Loading user u-1. Line 12 passes the product ID, and TypeScript marks it, on purpose here: Argument of type 'ProductId' is not assignable to parameter of type 'UserId'. In plain words: "you passed a product ID, and this function wants a user ID". Run it anyway, and it prints Loading user p-7: the wrong record, with no warning. With the brand, the mistake is caught before the code runs.
loadUser takes only a UserId, so passing productId on line 12 is an error.A hotel keeps many keys that look the same. Staff hang a colored tag on each one: red for the front door, blue for the storeroom. The tag doesn't change the key at all, but now nobody grabs the wrong one. A rack with red hooks only takes keys with a red tag.
- In real life: A key: it opens a door
- In TypeScript: A string:
"u-1" - In real life: A red tag that says "front door"
- In TypeScript: The brand:
{ readonly __brand: "UserId" } - In real life: A hook that only takes red tags
- In TypeScript:
loadUser(id: UserId) - In real life: The tag doesn't change how the key turns
- In TypeScript: A
UserIdis still a plain string when the code runs
A branded type is a normal type with an extra marker that only the type checker sees. Two values can be the same string or number when the code runs, but if their brands differ, TypeScript won't let one stand in for the other.
Why look-alike values get mixed up
Why does TypeScript need help here? A type alias like type Cents = number gives a type a second name, and nothing more (the Type aliases & interfaces lesson covers this). Here a shop stores prices in cents, but one price was written in dollars:
type Cents = number;type Dollars = number; function chargeCard(amount: Cents) { return `Charging ${amount} cents`;} const price: Dollars = 4.5;// ^?console.log(chargeCard(price));Lines 1 and 2 name two units. Line 4 says chargeCard takes Cents. Line 8 makes a price of 4.5 dollars. The query on line 9 shows what TypeScript sees: const price: number. The name Dollars is already gone, because it only ever meant number. Line 10 passes the dollars to chargeCard with no error, and prints Charging 4.5 cents. The customer pays 4 and a half cents instead of 4.50 dollars.
This is structural typing at work: TypeScript compares types by what they contain, not by their names (the Structural typing lesson covers it). Cents and Dollars both contain "a number", so they are the same type. The same goes for UserId and ProductId as plain strings. To keep them apart, their types must contain something different.
One way is to wrap each value in an object, like { kind: "user", value: "u-1" }. That works, but now every ID is an object when the code runs: you write .value everywhere, and the server's plain strings must be wrapped first. A brand keeps the value a plain string and changes only the type.
Adding a brand to a type
A brand is built from something you know: an intersection, A & B, a value that is an A and a B at the same time (the Intersection types lesson covers it). Here is what a plain string can and can't do with a branded type:
type UserId = string & { readonly __brand: "UserId" }; const fromText: UserId = "u-1";const fromAs = "u-1" as UserId; console.log(fromAs.toUpperCase());console.log(fromAs.length);Line 1 has three parts:
string: aUserIdis a string.{ readonly __brand: "UserId" }: and it has a property called__brandwhose type is exactly the text"UserId".readonlymeans no code may change it. The two underscores are a habit that says "not a real property, don't use it".&: both at once.
No real string has a __brand property, so line 3 gets an error, on purpose: Type 'string' is not assignable to type 'UserId'. The next line of the message says why: Type 'string' is not assignable to type '{ readonly __brand: "UserId"; }'. The text is a string, but it has no brand.
So how do you ever make a UserId? Line 4 uses as, a type assertion: you tell TypeScript "trust me, this is a UserId", and it does (the Type assertions lesson covers as). Lines 6 and 7 show that a UserId is still a string: toUpperCase() prints U-1, and length prints 3.
Now try different values. Each button changes what line 10 passes to loadUser:
type UserId = string & { readonly __brand: "UserId" };type ProductId = string & { readonly __brand: "ProductId" }; function loadUser(id: UserId) { return `Loading user ${id}`;} const userId = "u-1" as UserId;const productId = "p-7" as ProductId;console.log(loadUser(userId));userIdNo errorsLoading user u-1userId was made as a UserId on line 8. Its brand is "UserId", which is what loadUser asks for. No errors.
Only two values fit: a value that already is a UserId, and text that you assert is one. A product ID and plain text are both errors. Even productId as UserId is refused: Conversion of type 'ProductId' to type 'UserId' may be a mistake means no value can be both, because the two brands can never match.
Writing string & { readonly __brand: … } for every brand gets long. A small generic type, a type with a parameter, writes it once (the Generic types & interfaces lesson covers them):
type Brand<T, Name extends string> = T & { readonly __brand: Name }; type UserId = Brand<string, "UserId">;type OrderId = Brand<string, "OrderId">;type Cents = Brand<number, "Cents">; const orderId = "o-42" as OrderId;// ^?const price = 450 as Cents;console.log(orderId, price);Line 1 says: Brand takes a type T and a name, and gives back T with that name as its brand. Name extends string means the name must be text. Lines 3 to 5 make three brands with it, two on strings and one on a number. The query on line 8 shows const orderId: OrderId, and line 10 prints o-42 450.
The table below compares the three ways to keep IDs apart. Read each row as a question, then each way's answer.
| Question | type UserId = string | { kind: "user"; value: string } | A branded string |
|---|---|---|---|
| Does TypeScript catch a mixed-up ID? | No. Both names mean string. | Yes. The kind values differ. | Yes. The brands differ. |
| What is the value when the code runs? | A plain string, like "u-1". | An object, like { kind: "user", value: "u-1" }. | A plain string, like "u-1". |
| Do string methods work on it? | Yes. It's a string. | No. You read .value first. | Yes. It's still a string. |
| How do you make one? | Any string fits. | You write the whole object. | With as, best inside one checked function. |
Hiding the brand with unique symbol
The brand only exists in the types. That's what makes it free when the code runs, but it also means the __brand property is a promise nobody keeps:
type UserId = string & { readonly __brand: "UserId" }; const userId = "u-1" as UserId;const brandName = userId.__brand;// ^?console.log(brandName);console.log(typeof userId);Line 4 reads userId.__brand, and TypeScript allows it. The query on line 5 says const brandName: "UserId". But line 6 prints undefined, and line 7 prints string: when the code runs, userId is the plain string "u-1", with no properties of its own.
userId is the plain string "u-1", so userId.__brand is undefined.Your editor also suggests __brand after every userId., which invites mistakes like line 4. A common fix is to name the brand's property with a symbol that only one file can see. In JavaScript, every Symbol() is a new value equal to no other, and it can be a property's key (the Symbols lesson covers them). Here the brand lives in a file of its own:
declare const brand: unique symbol; export type Brand<T, Name extends string> = T & { readonly [brand]: Name };Line 1 says "there is a constant called brand, and its type is unique symbol": a type that only this one symbol has. declare means the line only describes it, and makes no value; it is removed with the types (the Declaration files lesson covers declare). Line 3 builds the Brand helper with [brand] as the property's key. The file exports Brand, but not brand.
import type { Brand } from "./brand"; type UserId = Brand<string, "UserId">;type ProductId = Brand<string, "ProductId">; function loadUser(id: UserId) { return `Loading user ${id}`;} const productId = "p-7" as ProductId;console.log(loadUser(productId));console.log(productId.__brand);Line 1 imports only the type. Lines 3 and 4 make two brands, and line 11 still catches the mix-up, on purpose. This time the message says Types of property '[brand]' are incompatible. Line 12 shows the difference, also on purpose: Property '__brand' does not exist on type 'ProductId'. There is no __brand to read or suggest, and ids.ts has no way to name brand, because brand.ts never exported it. Run it anyway, and it prints Loading user p-7 and undefined, as before: the symbol changes the types, not the values.
Constructor functions: the one way in
as UserId checks nothing. You could write "hello" as UserId and TypeScript would believe you. So most projects allow as in exactly one place for each brand: a function that checks the value first. People call it a constructor function or a smart constructor. It's a plain function; it has nothing to do with a class's constructor or with new.
type Email = string & { readonly __brand: "Email" }; function toEmail(text: string): Email { const email = text.trim().toLowerCase(); if (!/^\S+@\S+\.\S+$/.test(email)) { throw new Error(`Not an email: "${text}"`); } return email as Email;} function sendReceipt(to: Email) { return `Receipt sent to ${to}`;} const email = toEmail(" Asha@Example.com ");// ^?console.log(sendReceipt(email));Line 1 is the brand. Lines 3 to 9 are the constructor, toEmail. Line 4 removes spaces at both ends and makes the letters small. Line 5 tests the text against a regular expression, a pattern for text (the Patterns & flags lesson covers them). \S+@\S+\.\S+ means "some characters that aren't spaces, an @, more of them, a dot, and more". If the text doesn't match, line 6 throws an error. Only text that passed reaches line 8, the one as Email.
Lines 11 to 13 are sendReceipt, which asks for an Email. Line 15 makes one from messy text, and the query on line 16 shows const email: Email. Line 17 prints Receipt sent to asha@example.com.
toEmail is the one way to get an Email. Text that passes the check comes out branded, like the address on line 15. Text that fails, like "asha", throws instead.Now type your own text. Only line 16 changes, and the console shows what the code prints:
type Email = string & { readonly __brand: "Email" }; function toEmail(text: string): Email { const email = text.trim().toLowerCase(); if (!/^\S+@\S+\.\S+$/.test(email)) { throw new Error(`Not an email: "${text}"`); } return email as Email;} function sendReceipt(to: Email) { return `Receipt sent to ${to}`;} try { const email = toEmail(" Asha@Example.com "); console.log(sendReceipt(email));} catch (error) { console.log(error instanceof Error ? error.message : error);}Receipt sent to asha@example.comtoEmail trims the spaces and makes the letters small, the pattern matches, and you get an Email: asha@example.com.
Notice that the type checker never complains here. It can't know whether some text is an email; only the running code can find that out. The brand's job is different: it makes sure every Email in your program went through that check.
Throwing isn't always what you want. In a form, you'd rather show a message. A type guard, a function whose return type is value is Type, checks the text and narrows it, with no error to catch (the Type predicates & assertion functions lesson covers them):
type Email = string & { readonly __brand: "Email" }; function isEmail(text: string): text is Email { return /^\S+@\S+\.\S+$/.test(text);} function sendReceipt(to: Email) { return `Receipt sent to ${to}`;} function submit(typed: string) { if (isEmail(typed)) { return sendReceipt(typed); // ^? } return `Please check ${typed}`;} console.log(submit("ravi@example.com"));console.log(submit("ravi"));Line 3 says isEmail returns text is Email: when it returns true, TypeScript treats the text as an Email. Inside the if on line 12, the query on line 14 shows (parameter) typed: Email, so line 13 can call sendReceipt. Line 19 prints Receipt sent to ravi@example.com, and line 20 prints Please check ravi. This guard has no as at all: the predicate does the branding.
Here is what happens when as leaks out of the constructor:
type Email = string & { readonly __brand: "Email" }; function sendReceipt(to: Email) { return `Receipt sent to ${to}`;} const wrong = "not an email" as Email;console.log(sendReceipt(wrong));Line 7 asserts that "not an email" is an Email. There are no errors, and line 8 prints Receipt sent to not an email. A brand is only as good as the code that creates it, so keep each as Email inside the function that checks.
Units: cents are not dollars
Brands work on numbers too. Shops often store money as whole cents, because numbers with a decimal point can be slightly off (0.1 + 0.2 is 0.30000000000000004; the Number & Math in depth lesson explains why). Here are the cents and dollars from before, with brands:
type Cents = number & { readonly __brand: "Cents" };type Dollars = number & { readonly __brand: "Dollars" }; function dollarsToCents(amount: Dollars): Cents { return Math.round(amount * 100) as Cents;} function chargeCard(amount: Cents) { return `Charging ${amount} cents`;} const price = 4.5 as Dollars;console.log(chargeCard(price));console.log(chargeCard(dollarsToCents(price)));Lines 1 and 2 brand two numbers. Lines 4 to 6 convert: dollarsToCents takes Dollars, multiplies by 100, rounds to a whole number with Math.round, and brands the result as Cents. Line 13 makes the same mistake as before, and this time TypeScript marks it, on purpose: Argument of type 'Dollars' is not assignable to parameter of type 'Cents'. Line 14 converts first, and prints Charging 450 cents.
dollarsToCents turns Dollars into Cents: 4.5 becomes 450. chargeCard takes only Cents, so line 14 works, and line 13, which passes the dollars, is an error.Brands have one surprise with numbers. Math gives back a plain number:
type Cents = number & { readonly __brand: "Cents" }; function addCents(a: Cents, b: Cents): Cents { return (a + b) as Cents;} const tea = 300 as Cents;const cake = 450 as Cents;const sum = tea + cake;// ^?const total = addCents(tea, cake);// ^?console.log(sum, total);Line 9 adds two Cents, and the query on line 10 shows const sum: number. TypeScript knows that + on two numbers gives a number, but not that cents plus cents is still cents, so the brand is gone. Lines 3 to 5 are a small helper that adds and puts the brand back, so the query on line 12 shows const total: Cents. Line 13 prints 750 750: the same value, with different types.
The same idea works for any value with rules: a Percent from 0 to 100, a Quantity that is a whole number, a Slug made of small letters and dashes for a web address, or an Email. Each gets a brand and one function that checks and brands.
IDs and prices in a shopping cart
Here is how the pieces fit in a real cart. The server sends plain data. One function per brand checks it as it comes in, and from then on, the types carry the checks with them:
type ProductId = string & { readonly __brand: "ProductId" };type Cents = number & { readonly __brand: "Cents" }; function toProductId(text: string): ProductId { if (!text.startsWith("p-")) throw new Error(`Not a product id: ${text}`); return text as ProductId;} function checkCents(amount: number): Cents { if (!Number.isInteger(amount) || amount < 0) throw new Error(`Not a price in cents: ${amount}`); return amount as Cents;} type CartLine = { id: ProductId; price: Cents; quantity: number }; function readLine(item: { id: string; price: number; quantity: number }): CartLine { return { id: toProductId(item.id), price: checkCents(item.price), quantity: item.quantity };} function lineTotal(line: CartLine): Cents { return checkCents(line.price * line.quantity);} const reply = [{ id: "p-7", price: 450, quantity: 2 }, { id: "p-9", price: 300, quantity: 1 }];const cart = reply.map(readLine);for (const line of cart) { console.log(`${line.id}: ${lineTotal(line)} cents`);}Lines 1 and 2 are the brands. Lines 4 to 7 and 9 to 12 are their constructors: a product ID must start with p-, and a price must be a whole number of cents, 0 or more. Line 14 is a cart line that uses both brands. Lines 16 to 18, readLine, turn one plain item from the server into a CartLine, checking each field on the way.
Line 24 stands in for the server's reply. In a real app, it comes from response.json(), and you check its shape first (the Typing fetch & JSON lesson shows how). Line 25 reads every item. Lines 20 to 22 compute a line's total, and use checkCents to brand the result of the multiplication. Lines 26 to 28 print p-7: 900 cents and p-9: 300 cents.
Everything after line 25 can trust the cart: every id is a checked ProductId, and every price is a whole number of cents. The checks happen once, at the edge of the program, instead of in every function.
Now sort some lines. Each one is checked after these lines:
type UserId = string & { readonly __brand: "UserId" };type OrderId = string & { readonly __brand: "OrderId" };type Cents = number & { readonly __brand: "Cents" }; const userId = "u-1" as UserId;const orderId = "o-42" as OrderId;const price = 450 as Cents;console.log(userId, orderId, price);const a: UserId = "u-1";const b: UserId = userId;const c: UserId = orderId;const d: string = userId;const e: Cents = price + 50;const f: number = price * 2;const g = "u-2" as UserId;const h = orderId as UserId;
Sort each line by what TypeScript says about it. Each line is checked on its own, after the brands above.
Common mix-ups
Most mix-ups about brands come from forgetting that they are only types. Here is a user with a branded ID, as the code sees it when it runs:
type UserId = string & { readonly __brand: "UserId" }; const user = { id: "u-1" as UserId, name: "Asha" };console.log(typeof user.id);console.log(JSON.stringify(user));Line 3 makes a user whose id is a UserId. Line 4 prints string. Line 5 turns the user into JSON text and prints {"id":"u-1","name":"Asha"}: no brand anywhere. When that JSON comes back from a server, the ID is a plain string again, and needs its constructor (the JSON lesson covers JSON.stringify).
- "A brand checks the value." No. A brand only records that some code said the value is right. The check is the constructor's job, and
asskips it. - "A branded value has a
__brandproperty." No. The property exists only in the types. Reading it givesundefined. - "
type UserId = stringalready keeps IDs apart." No. An alias is a second name forstring, and any string fits. - "Math on
CentsgivesCents." No. It gives a plainnumber. Put the brand back with a helper likeaddCents. - "A branded string can't be used as a string." No. It fits anywhere a
stringgoes, and every string method works on it. Only the other direction is blocked. - "Brands cost something when the code runs." No. The value is the same plain string or number, with nothing added. The constructor's check is the only extra work.
Practice exercises
Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
Which line will TypeScript mark, and why?
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
function cancelOrder(id: OrderId) {
return `Cancelled ${id}`;
}
const userId = "u-1" as UserId;
const orderId = "o-42" as OrderId;
console.log(cancelOrder(orderId));
console.log(cancelOrder(userId));type UserId = string & { readonly __brand: "UserId" };type OrderId = string & { readonly __brand: "OrderId" }; function cancelOrder(id: OrderId) { return `Cancelled ${id}`;} const userId = "u-1" as UserId;const orderId = "o-42" as OrderId;console.log(cancelOrder(orderId));console.log(`Signed in as ${userId}`);Line 11 passes userId, whose brand is "UserId", to a function that wants an OrderId, so TypeScript marks it. Without the brands, it would cancel order u-1. The fix uses the user ID for what it's for.
What type does TypeScript give twoTeas on line 4? Guess first. Then point at twoTeas, or press Edit & run and add a // ^? line under it.
type Cents = number & { readonly __brand: "Cents" }; const tea = 300 as Cents;const twoTeas = tea * 2;type Cents = number & { readonly __brand: "Cents" }; const tea = 300 as Cents;const twoTeas = tea * 2;// ^?Math on numbers gives a plain number, so the brand is dropped. The query on line 5 shows const twoTeas: number.
Lines 1 to 3 repeat the same shape three times. Write a Brand helper and use it for all three. Then add a query under orderId. What does it show?
type UserId = string & { readonly __brand: "UserId" };type OrderId = string & { readonly __brand: "OrderId" };type Cents = number & { readonly __brand: "Cents" }; const orderId = "o-42" as OrderId;console.log(orderId);type Brand<T, Name extends string> = T & { readonly __brand: Name }; type UserId = Brand<string, "UserId">;type OrderId = Brand<string, "OrderId">;type Cents = Brand<number, "Cents">; const orderId = "o-42" as OrderId;// ^?console.log(orderId);Brand<T, Name> writes the intersection once. Each brand passes its base type and its name. The query on line 8 shows const orderId: OrderId: TypeScript names the type after your alias.
Right now toQuantity brands any number. Make it throw Not a quantity: … unless the number is whole and from 1 to 99. What does your finished code print for 2.5?
type Quantity = number & { readonly __brand: "Quantity" }; function toQuantity(n: number): Quantity { // Throw unless n is a whole number from 1 to 99. return n as Quantity;} for (const n of [2, 0, 2.5]) { try { console.log(`Added ${toQuantity(n)}`); } catch (error) { console.log(error instanceof Error ? error.message : error); }}type Quantity = number & { readonly __brand: "Quantity" }; function toQuantity(n: number): Quantity { if (!Number.isInteger(n) || n < 1 || n > 99) { throw new Error(`Not a quantity: ${n}`); } return n as Quantity;} for (const n of [2, 0, 2.5]) { try { console.log(`Added ${toQuantity(n)}`); } catch (error) { console.log(error instanceof Error ? error.message : error); }}The constructor throws unless n is a whole number from 1 to 99, and only then brands it. It prints Added 2, Not a quantity: 0, and Not a quantity: 2.5.
TypeScript marks line 13. Change one line so it doesn't, without using as. What does line 17 print?
type Slug = string & { readonly __brand: "Slug" }; function isSlug(text: string): boolean { return /^[a-z0-9-]+$/.test(text);} function pageUrl(slug: Slug) { return `/blog/${slug}`;} function linkFor(title: string) { const text = title.toLowerCase().replaceAll(" ", "-"); return isSlug(text) ? pageUrl(text) : "No link";} console.log(linkFor("Hello World"));console.log(linkFor("Tea & Cake"));type Slug = string & { readonly __brand: "Slug" }; function isSlug(text: string): text is Slug { return /^[a-z0-9-]+$/.test(text);} function pageUrl(slug: Slug) { return `/blog/${slug}`;} function linkFor(title: string) { const text = title.toLowerCase().replaceAll(" ", "-"); return isSlug(text) ? pageUrl(text) : "No link";} console.log(linkFor("Hello World"));console.log(linkFor("Tea & Cake"));With text is Slug, a true from isSlug narrows text to Slug, so line 13 can pass it to pageUrl. "Hello World" becomes hello-world and prints /blog/hello-world. "Tea & Cake" becomes tea-&-cake, which has a &, so line 17 prints No link.
This code charges 4.5 cents instead of 450, and TypeScript says nothing. Brand the two units so TypeScript catches it, then fix the call. What does your fixed code print?
type Cents = number;type Dollars = number; function dollarsToCents(amount: Dollars): Cents { return Math.round(amount * 100);} function chargeCard(amount: Cents) { return `Charging ${amount} cents`;} const price: Dollars = 4.5;console.log(chargeCard(price));type Cents = number & { readonly __brand: "Cents" };type Dollars = number & { readonly __brand: "Dollars" }; function dollarsToCents(amount: Dollars): Cents { return Math.round(amount * 100) as Cents;} function chargeCard(amount: Cents) { return `Charging ${amount} cents`;} const price = 4.5 as Dollars;console.log(chargeCard(dollarsToCents(price)));With both types branded, line 13 is an error: Dollars don't fit Cents. Passing dollarsToCents(price) fixes it, and it prints Charging 450 cents instead of Charging 4.5 cents.
Check your understanding
For each question, ask two things: what does the checker see, brand and all, and what does the code do when it runs, after the types are gone? The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
string & { readonly __brand: "UserId" }describe?Choose an answer to see the explanation.
Question 2 of 7This sends a receipt to typed text. Which line does TypeScript flag?
Read the code, then predicttype Email = string & { readonly __brand: "Email" }; function sendReceipt(to: Email) { return `Receipt sent to ${to}`; } const typed = "asha@example.com"; console.log(sendReceipt(typed));Choose an answer to see the explanation.
Question 3 of 7What does this branded order ID print with typeof?
Read the code, then predicttype OrderId = string & { readonly __brand: "OrderId" }; const orderId = "o-42" as OrderId; console.log(typeof orderId);Choose an answer to see the explanation.
Question 4 of 7What type does TypeScript give
withTax?Read the code, then predicttype Cents = number & { readonly __brand: "Cents" }; const price = 450 as Cents; const withTax = price + 45;Choose an answer to see the explanation.
Question 5 of 7What does this call with
as Emailprint?Read the code, then predicttype Email = string & { readonly __brand: "Email" }; function sendReceipt(to: Email) { return `Receipt sent to ${to}`; } console.log(sendReceipt("oops" as Email));Choose an answer to see the explanation.
Question 6 of 7Why put the brand's key in a
unique symbolthat its file doesn't export?Choose an answer to see the explanation.
Question 7 of 7In a project with an
Emailbrand, where shouldas Emailappear?Choose an answer to see the explanation.
Key takeaways
- TypeScript compares types by shape, so
type UserId = stringandtype ProductId = stringare the same type. - A brand,
string & { readonly __brand: "UserId" }, adds a marker only the checker sees, so look-alike values stop fitting each other. - A
Brand<T, Name>helper writes it once. Aunique symbolkey that its file doesn't export hides the brand from other files. - Make branded values in one place: a constructor that checks and throws, or a type guard. Keep
asinside it. - Math on a branded number gives a plain
number; a small helper puts the brand back. - When the code runs, a brand is gone: the value is the same plain string or number, and data from a server needs its constructor again.
Remember the one-liner.
A brand is a label only the checker can read, and one checked function is the only place that may stick it on.
Coming next: Type-safe APIs & routes. One description of a server's routes types the client, the path's parameters, and every response.