cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Brand a typeWrite string & { readonly __brand: "UserId" } or a Brand<T, Name> helper, so look-alike values stop fitting each other.
  • 02
    Make branded values safelyWrite one constructor function or type guard that checks a value and gives back the branded type.
  • 03
    Know the limitsExplain why a brand is gone when the code runs, why as skips 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:

A product ID passed as a user ID (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

Two string cards. The card u-1 has a tag that says UserId, and the card p-7 has a tag that says ProductId. Both point at a window called loadUser, which accepts only UserId. The u-1 arrow is marked fits; the p-7 arrow is marked line 12, an error."u-1"UserId"p-7"ProductIdfitsline 12loadUseraccepts onlyUserId
Both IDs are plain strings, and each carries a brand. loadUser takes only a UserId, so passing productId on line 12 is an error.
Real-life analogyKeys with colored tags

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 UserId is still a plain string when the code runs
The main idea

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:

Dollars passed where cents go, with no errorPop out in the code editor (opens in a new tab)TypeScript
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:

A plain string isn't a UserId (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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: a UserId is a string.
  • { readonly __brand: "UserId" }: and it has a property called __brand whose type is exactly the text "UserId". readonly means 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:

Playground: pass something to loadUser
Which values fit a UserId?Pop out in the code editor (opens in a new tab)TypeScript
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));
Line 10 passesuserId
What happens
TypeScriptNo errors
When it runsLoading user u-1
Try it yourself

userId was made as a UserId on line 8. Its brand is "UserId", which is what loadUser asks for. No errors.

TypeScript checks the code for real. Each button changes only what line 10 passes. Reset goes back to userId.

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):

One Brand helper for every brandPop out in the code editor (opens in a new tab)TypeScript
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.

A type alias, an object with a kind, and a brand, side by side
Questiontype 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:

Reading a brand that isn't therePop out in the code editor (opens in a new tab)TypeScript
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.

Left, a window ids.ts with const userId = u-1 as UserId. An arrow marked types removed leads to a window ids.js with const userId = u-1, a plain string. Under the left window: userId is a UserId. Under the right window: typeof gives string.ids.tsconst userId ="u-1" as UserId;typesremovedids.jsconst userId ="u-1";a UserId to the checkertypeof gives string
Before the code runs, the types are removed, and the brand goes with them. In the JavaScript, 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.

ids.ts (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

toEmail checks text before it brands itPop out in the code editor (opens in a new tab)TypeScript
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.

Two text cards go into a window called toEmail, which trims, lowercases, and checks the pattern. The text with spaces, Asha@Example.com, comes out as the card asha@example.com, marked Email. The text asha comes out as: throws an Error." Asha@Example.com ""asha"toEmailtrim andlowercasecheck thepatternasha@example.comEmailthrows an Error
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:

Playground: type an email address
toEmail checks text while the code runsPop out in the code editor (opens in a new tab)TypeScript
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);}
Console
An Email was made
PrintsReceipt sent to asha@example.com
Try it yourself

toEmail trims the spaces and makes the letters small, the pattern matches, and you get an Email: asha@example.com.

Only line 16 changes. The type checker sees no error either way: whether the text is an email is something only the running code can find out. Reset puts the first address back.

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):

A type guard that brands an emailPop out in the code editor (opens in a new tab)TypeScript
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:

as skips the checkPop out in the code editor (opens in a new tab)TypeScript
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:

Dollars can't go where cents go (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

A card 4.5 with a tag Dollars. An arrow leads into a window dollarsToCents, which multiplies by 100 and rounds. An arrow leads out to a card 450 with a tag Cents.4.5DollarsdollarsToCents× 100then round450Cents
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:

Adding two Cents gives a numberPop out in the code editor (opens in a new tab)TypeScript
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:

A cart that checks its data once, at the edgePop out in the code editor (opens in a new tab)TypeScript
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:

The brands the cards usePop out in the code editor (opens in a new tab)TypeScript
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);
Error or fine?
  • 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;
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own, after the brands above.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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:

The brand is gone when the code runsPop out in the code editor (opens in a new tab)TypeScript
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 as skips it.
  • "A branded value has a __brand property." No. The property exists only in the types. Reading it gives undefined.
  • "type UserId = string already keeps IDs apart." No. An alias is a second name for string, and any string fits.
  • "Math on Cents gives Cents." No. It gives a plain number. Put the brand back with a helper like addCents.
  • "A branded string can't be used as a string." No. It fits anywhere a string goes, 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.

Exercise 1 · Warm-upFind the mixed-up call

Which line will TypeScript mark, and why?

Starter codePop out in the code editor (opens in a new tab)TypeScript
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));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict the type after math

    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.

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    type Cents = number & { readonly __brand: "Cents" }; const tea = 300 as Cents;const twoTeas = tea * 2;

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWrite a Brand helper

      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?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      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);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWrite a constructor for quantities

        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?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        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);  }}

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeTurn a check into a type guard

          TypeScript marks line 13. Change one line so it doesn't, without using as. What does line 17 print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          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"));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeCatch the dollars-for-cents bug

            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?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            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));

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              The branded types quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does string & { readonly __brand: "UserId" } describe?

                Choose an answer to see the explanation.

              2. Question 2 of 7This sends a receipt to typed text. Which line does TypeScript flag?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type 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.

              3. Question 3 of 7What does this branded order ID print with typeof?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type OrderId = string & { readonly __brand: "OrderId" };
                
                const orderId = "o-42" as OrderId;
                console.log(typeof orderId);

                Choose an answer to see the explanation.

              4. Question 4 of 7What type does TypeScript give withTax?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Cents = number & { readonly __brand: "Cents" };
                
                const price = 450 as Cents;
                const withTax = price + 45;

                Choose an answer to see the explanation.

              5. Question 5 of 7What does this call with as Email print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type 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.

              6. Question 6 of 7Why put the brand's key in a unique symbol that its file doesn't export?

                Choose an answer to see the explanation.

              7. Question 7 of 7In a project with an Email brand, where should as Email appear?

                Choose an answer to see the explanation.

              Key takeaways

              • TypeScript compares types by shape, so type UserId = string and type ProductId = string are 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. A unique symbol key 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 as inside 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.

              CompleteFrontend Clear concepts. Working examples.