Challenge 3: Branded + phantom
- TypeScript course
- Advanced
- Lesson 56/57
- 6 min
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 interchangeableCombine them to build APIs that are runtime-safe and state-specific at compile time.
Practice 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.
Starter code
// Brand<T, B> tanımla
// UserId = Brand<string, "UserId">
// Email = Brand<string, "Email">
// her ikisinden bir değer oluştur, UserId değerini yazdırSample 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);Expected output
u123
Practice 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.
Starter code
// Brand ve phantom birleştir
// Box<S>: S = "Open" | "Sealed" phantom durumu
// open: mühürlü kutuyu açılır kutuya çevirir
// bir mühürlü kutu oluştur, open çağır, value'yu yazdırSample 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);Expected output
7
Questions and answers
Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.
1 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; Correct answer
- let u: UserId = 5 as UserId;
- let u: UserId = "x" as OrderId;
- let o: OrderId = "x" as UserId;
Explanation: 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.
2 Brand symbol
declare const __brand: ____;
type Brand<T, B> = T & { readonly [__brand]: B };
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
Explanation: The brand must be truly unique, so `unique symbol` is required; a plain `symbol` cannot be used as a computed key in type position.
3 Branded and phantom parts
| Term | Meaning |
|---|---|
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 |