Q30 of 38 · TypeScript

What is structural typing in TypeScript, and how do you simulate nominal typing when needed?

TypeScriptSeniortypescriptstructural-typingnominal-typingbranded-typestype-safetyadvanced-types

Short answer

Short answer: TypeScript uses structural typing — two types are compatible if they have the same shape, regardless of name. Nominal typing (identity by name) can be simulated with branded types: adding a unique `readonly _brand: unique symbol` property that only the defining module can satisfy.

Detail

TypeScript is structurally typed: compatibility is determined by the shape of a type (its properties and methods), not its name. Two types with the same structure are mutually assignable even if declared separately.

Why structural typing is mostly good: It aligns with JavaScript's duck typing. A Playwright Locator returned from page.getByRole is compatible with any code that accepts an object with the same methods — no explicit interface implementation needed.

When structural typing causes problems:

  • A UserId and OrderId are both number — you can accidentally pass one where the other is expected.
  • A Celsius and Fahrenheit are both number — temperature calculations silently mix units.
  • CompanyName and UserName are both string — swapping them compiles fine.

Branded types: Simulate nominal typing by adding a brand property that cannot be assigned without using a type assertion — effectively making the type unique:

type UserId = number & { readonly _brand: unique symbol };
function makeUserId(n: number): UserId { return n as UserId; }

In test automation: Branded types are used in typed test data: TestUserId, SessionToken, FixtureKey — ensuring that helper functions receive the correct kind of identifier even if the underlying type is the same.

// EXAMPLE

// Structural typing — compatible by shape
interface Point { x: number; y: number; }
interface Vector { x: number; y: number; }
const p: Point = { x: 1, y: 2 };
const v: Vector = p;  // OK — same structure

// Problem: structurally identical but semantically different
type UserId = number;
type OrderId = number;
function getUser(id: UserId): User { /* ... */ }
const orderId: OrderId = 42;
getUser(orderId); // compiles — but wrong! orderId ≠ userId

// Solution: branded type
declare const _brand: unique symbol;
type UserId2 = number & { readonly [_brand]: "UserId" };
type OrderId2 = number & { readonly [_brand]: "OrderId" };

function makeUserId(n: number): UserId2 { return n as UserId2; }
function makeOrderId(n: number): OrderId2 { return n as OrderId2; }

function getUser2(id: UserId2) { /* ... */ }
const uid = makeUserId(1);
const oid = makeOrderId(2);
getUser2(uid); // ok
// getUser2(oid); // Error! OrderId2 not assignable to UserId2

// WHAT INTERVIEWERS LOOK FOR

Clear definition of structural vs nominal typing. Real-world examples where structural typing causes bugs (mixed IDs, mixed units). The branded type pattern as a practical workaround. This is a senior-level concept that shows awareness of TypeScript's design tradeoffs.

// COMMON PITFALL

Using classes instead of branded types for nominal-like behaviour — while two class instances with the same structure are not assignable to each other if they're different classes (class instances carry nominal identity via the constructor), this doesn't work for primitive types.