Capítulo 30 de 61

Chapter 30: Branded Types & Readonly

Core Idea

.brand() simulates nominal typing on top of TypeScript's structural type system, so structurally-identical types (e.g. two { name: string } shapes) become type-incompatible; .readonly() marks the inferred type as read-only and actually freezes the parsed result at runtime.

Key Concepts

  • .brand<"Tag">(): attaches a phantom brand to the schema's output type by default; a value must be produced by .parse() on that exact branded schema to satisfy the branded type — plain unbranded data of the same shape is rejected by the type checker.
  • No runtime effect: branding is purely a TypeScript-level construct — .parse()'s actual return value is unaffected.
  • Brand direction (Zod 4.2+): .brand<"Tag", "out">() (default, brands output), "in" (brands input), "inout" (brands both).
  • .readonly(): marks the inferred type Readonly<...> for objects/arrays/tuples/Map/Set, and actually calls Object.freeze() on the parsed result, so mutation attempts throw a TypeError at runtime — not just a type error.

Code Examples

const Cat = z.object({ name: z.string() }).brand<"Cat">();
const Dog = z.object({ name: z.string() }).brand<"Dog">();

const pluto = Dog.parse({ name: "pluto" });
const simba: z.infer<typeof Cat> = pluto; // type error — Dog isn't assignable to Cat

const ReadonlyUser = z.object({ name: z.string() }).readonly();
const result = ReadonlyUser.parse({ name: "fido" });
result.name = "simba"; // throws TypeError at runtime — object is frozen
  • What it demonstrates: branding blocks structurally-equal-but-semantically-different types at compile time; readonly enforces immutability at both compile time and runtime.

Anti-patterns

  • Expecting .brand() to validate anything at runtime: it doesn't — branding only affects the static type; two objects with the same shape are still runtime-identical regardless of brand.

Key Takeaways

  1. Use .brand() when you want the type system to prevent mixing up semantically distinct values that happen to share a shape (e.g. UserId vs OrderId, both string).
  2. .readonly() is not just cosmetic typing — the parsed object is actually frozen, so runtime mutation attempts fail loudly.
  3. Branding only guards against structural type confusion at compile time; it adds zero runtime validation cost or behavior.

Connects To

  • Objects — Definition, Strictness & Shape: branding and readonly both commonly apply to object schemas.
  • Custom: another way to add semantic meaning beyond a schema's raw structural type.