TypeScript
Challenge 3: Branded + phantom
Solve a task combining branded and phantom types.
Branded + phantom types
Branded types add a unique symbol signature to a primitive value, creating nominal types that share a base type but must not be mixed. Phantom types encode state in a type parameter that exists only at compile time and is never used at runtime.
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
// UserId and OrderId are no longer interchangeable
Combine them to build APIs that are runtime-safe and state-specific at compile time.
Exercise Define a brand type
Declare a unique symbol named __brand and write Brand<T, B> = T & { readonly [__brand]: B }. Create aliases UserId = Brand<string, "UserId"> and Email = Brand<string, "Email">, make a value of each, and print uid.
Show example solution
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
type UserId = Brand<string, "UserId">;
type Email = Brand<string, "Email">;
const uid: UserId = "u123" as UserId;
const em: Email = "a@b.com" as Email;
console.log(uid);
Exercise Box with phantom state
Write a Box<S> type: S is a phantom parameter constrained to "Open" | "Sealed", and the type is Brand<{ value: number }, `Box_${S}`>. An open(b: Box<"Sealed">): Box<"Open"> function returns b.value as a new box. Create a sealed box, call open, and print value.
Show example solution
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
type Box<S extends "Open" | "Sealed"> = Brand<{ value: number }, `Box_${S}`>;
function open(b: Box<"Sealed">): Box<"Open"> {
return { value: b.value } as Box<"Open">;
}
const sealed: Box<"Sealed"> = { value: 7 } as Box<"Sealed">;
const opened = open(sealed);
console.log(opened.value);
Test yourself
-
After the definitions below, which line compiles?
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
- let u: UserId = "x" as UserId;
- let u: UserId = 5 as UserId;
- let u: UserId = "x" as OrderId;
- let o: OrderId = "x" as UserId;
Show answer
let u: UserId = "x" as UserId; — Branded types act nominally; only values with the same brand and same base type are assignable. A different brand or a wrong base type is a compile error.
-
Brand symbol
declare const __brand: ____;
type Brand<T, B> = T & { readonly [__brand]: B };
Show answer
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
-
Branded and phantom parts
- Branded type
- symbol signature for nominal behavior
- Phantom type
- type parameter unused at runtime
- unique symbol
- guarantees a unique key
- as UserId
- lifts a value to the branded type
Next lesson: Challenge 4: Mini compiler