Q30 of 38 · TypeScript
What is structural typing in TypeScript, and how do you simulate nominal typing when needed?
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
UserIdandOrderIdare bothnumber— you can accidentally pass one where the other is expected. - A
CelsiusandFahrenheitare bothnumber— temperature calculations silently mix units. CompanyNameandUserNameare bothstring— 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