CodeBlanks Logo Icon CodeBlanks

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.

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

index.ts
// Brand<T, B> tanımla
// UserId = Brand<string, "UserId">
// Email = Brand<string, "Email">
// her ikisinden bir değer oluştur, UserId değerini yazdır

Sample solution

index.ts
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

index.ts
// 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ır

Sample solution

index.ts
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?

index.ts
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

Question
declare const __brand: ____;
type Brand<T, B> = T & { readonly [__brand]: B };
Answer
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

All lessons in Advanced