Structural Typing
Date: 2026-09-28
A TypeScript type is a set of values, and “is A assignable to B” means “is every A also a B”. Shape decides membership, never the name — so an object with extra properties still fits, and the only place TypeScript objects to extras is a fresh object literal.
Structural typing means two types are compatible when their shapes are compatible, regardless of what they’re called or where they were declared. The nominal-versus-structural axis itself is in Type Systems; this note is the model TypeScript runs on.
Types as sets
The model that makes every compatibility rule predictable: a type names the set of values that could inhabit it. Assignability is the subset test.
type the set of values
──────────── ──────────────────────────────────────
never ∅ — no values at all
'gbp' { 'gbp' }
'gbp' | 'eur' { 'gbp', 'eur' }
string every string
{ id: string } every object with a string `id` —
AND WHATEVER ELSE IT HAS ← the one people miss
unknown everything
Read off the table:
- Adding properties to an object type makes the set smaller, not larger.
{ id: string; email: string }is a subset of{ id: string }— which is why the wider-looking type is assignable to the narrower-looking one - Adding members to a union makes the set larger.
'gbp' | 'eur'accepts more than'gbp' neveris assignable to everything (the empty set is a subset of every set) and everything is assignable tounknown.anyisn’t in the table because it isn’t a set — it switches the test off in both directions
Extra properties are fine — mostly
type Customer = { id: string }
const row = { id: 'c_1', email: 'a@b.com', ltv: 420 }
const c: Customer = row // ✓ row is in the set
const c2: Customer = { id: 'c_1', email: 'a@b.com' }
// ~~~~~ ✗ Object literal may only specify known propertiesSame value, different verdict. Excess property checking is a separate lint-like rule that applies only to a fresh object literal assigned straight to a typed target. The reasoning: a literal written inline with an unknown key is almost always a typo (colour for color), whereas a variable passed along probably has extras for good reason.
Notice what this means: the typo protection vanishes the moment the literal goes through a variable first. It’s the one place TypeScript behaves nominally-ish, and it’s shallow.
Where the model bites
Units and IDs collapse. type OrderId = string and type CustomerId = string are the same set, so passing one for the other compiles. The fix is branding — intersecting with a phantom property so the sets differ — covered with its syntax in Type Systems. Worth it for money, IDs and units; ceremony everywhere else.
Functions compare the other way round for parameters. A function accepting Customer | Guest can stand in where one accepting Customer is expected — it handles more, so it’s safe. That’s contravariance: parameter sets must be supersets.
expected: (c: Customer) => void
supplied: (c: Customer | Guest) => void ✓ handles everything expected and more
supplied: (c: VipCustomer) => void ✗ would be handed non-VIPs
The catch: strictFunctionTypes enforces this only for function-type properties. Methods declared with method syntax (save(c: Customer): void) stay bivariant — the unsafe direction compiles — a deliberate hole kept so that arrays and DOM (Document Object Model) types remain usable.
Classes are structural too. An object literal with the right shape satisfies a class type, instanceof checks notwithstanding. The exception: a class with a private or #private member only accepts instances from that declaration — the one genuinely nominal escape hatch in the language.
Why it’s the right default for JavaScript
- JS code is duck-typed already. Structural types describe what the code actually requires, rather than imposing a class hierarchy it never had
- Types from different libraries interoperate without adapters — anything with
{ x: number; y: number }is a point - The cost is paid at the edges: accidental compatibility between things that happen to share a shape. Branding covers the cases where that matters
Related: Generics for types that vary by parameter, Type Narrowing for how a union’s set shrinks inside a branch, Type Safety Boundaries for where the set claimed and the value received part company.