Articles

Structural Typing vs Nominal Typing in TypeScript

Structural typing checks shape, not name — TypeScript treats two differently-named types as compatible if their members match, unlike nominal systems.

Takina Takina · · 4 min read
Code displayed on a monitor in an editor

Structural typing is a type system rule where two types are considered compatible if they have the same shape — the same members with the same types — regardless of what they’re named or how they’re declared. Nominal typing is the opposite rule: two types are only compatible if they’re explicitly declared to be the same type or subtype, by name, even if their shapes are identical. TypeScript uses structural typing, which surprises a lot of developers coming from nominally-typed languages like Java or C#.

What structural typing looks like in practice

In TypeScript, if two interfaces or types describe objects with matching members, values of one are assignable to the other — no explicit relationship required:

interface Point {
  x: number;
  y: number;
}

interface Coordinate {
  x: number;
  y: number;
}

function logPoint(p: Point) {
  console.log(`${p.x}, ${p.y}`);
}

const c: Coordinate = { x: 1, y: 2 };
logPoint(c); // fine — Coordinate has everything Point requires

Point and Coordinate are never declared to be related to each other, but because Coordinate has all the members Point requires, TypeScript accepts a Coordinate value wherever a Point is expected. This works even for object literals passed directly, and for classes — an instance of a class satisfies an interface simply by having the right shape, without an explicit implements clause.

This is often called “duck typing” at the type-checking level: if it has the members a Point needs, it’s treated as a Point, whatever its own declared name says.

What nominal typing looks like instead

In a nominally-typed language like Java, the equivalent code would fail to compile. Two classes with identical fields are still considered different, unrelated types unless one explicitly implements an interface the other also implements, or extends a shared base class. The name and declared lineage of the type is what matters, not its shape.

class Point { double x, y; }
class Coordinate { double x, y; }

void logPoint(Point p) { ... }

Coordinate c = new Coordinate();
logPoint(c); // compile error — Coordinate is not a Point

Even though Point and Coordinate have identical fields, Java’s type checker refuses the call because there’s no declared relationship between the two types. You’d need Coordinate to explicitly implement an interface or extend a class that logPoint accepts.

Structural vs nominal, side by side

Structural typingNominal typing
Compatibility ruleBased on shape/membersBased on declared name/lineage
Example languagesTypeScript, Go, OCamlJava, C#, Swift, Rust
Explicit relationships neededNo — shape match is enoughYes — implements/extends or equivalent
Accidental compatibilityPossible — unrelated types can satisfy each other by coincidenceNot possible — must be declared
Refactoring safetyRenaming a type doesn’t break assignability elsewhereRenaming is safer to track since relationships are explicit

Neither approach is strictly better — they optimize for different things. Structural typing is flexible and works naturally with JavaScript’s dynamic, object-shape-driven style, which is exactly why TypeScript adopted it: TypeScript’s job is to add types on top of existing JavaScript patterns, and those patterns are inherently structural (any object with a .length property can be treated as array-like, for instance). Nominal typing gives stronger guarantees about intent — two types with the same shape but different names usually represent genuinely different concepts, and a nominal system won’t let you mix them up by accident.

Where structural typing catches you off guard

The flexibility cuts both ways. Because TypeScript checks shape rather than declared identity, two types that are conceptually unrelated but happen to share a shape will type-check as compatible, even when that’s not what you intended:

interface UserId { value: string; }
interface ProductId { value: string; }

function getUser(id: UserId) { ... }

const pid: ProductId = { value: "abc" };
getUser(pid); // type-checks, but this is almost certainly a bug

This is a common source of subtle mistakes in larger codebases, especially around ID types. The usual fix is to simulate nominal typing with a pattern often called “branded types” or “nominal typing emulation” — adding a unique, otherwise-unused property to each type so that shapes stop accidentally overlapping. If you want the full mechanics of that pattern, TypeScript’s generics and utility types make it straightforward to build.

Why this matters day to day

Structural typing is part of why TypeScript integrates so smoothly with plain JavaScript and why gradually adding types to an existing codebase feels natural rather than fighting the language — see TypeScript vs. JavaScript for the broader tradeoffs of adopting a type system on top of JavaScript at all. It also affects how interfaces relate to type aliases in practice, since both are checked structurally and are largely interchangeable for object shapes. Module boundaries matter too — how types are exported and consumed across ESM and CommonJS modules doesn’t change the structural rules, but it’s worth knowing the type system behaves consistently regardless of the module format underneath it.

The takeaway

TypeScript decides type compatibility by comparing shape, not name — if two types have matching members, they’re interchangeable, whatever they’re called or however they were declared. That’s a deliberate fit for JavaScript’s dynamic, duck-typed roots, and it’s what makes gradually typing existing code painless. The tradeoff is that unrelated types with coincidentally matching shapes will type-check as compatible even when they shouldn’t be, which is worth watching for around anything like IDs, tokens, or other single-field wrapper types where accidental mixing is an easy mistake to make.

Takina Takina · · 5 min read

TypeScript Variance Explained

Variance decides when TypeScript accepts Array<Dog> where Array<Animal> is expected — covariant, contravariant, or invariant, explained with examples.

#TypeScript #JavaScript #Web Development
Takina Takina · · 5 min read

Zod vs Yup: TypeScript Schema Validation Compared

Zod infers static TypeScript types directly from its schemas; Yup was built for JavaScript form validation first. How the two approaches differ.

#TypeScript #JavaScript #Web Development
Takina Takina · · 5 min read

TypeScript Declaration Files (.d.ts) Explained

A .d.ts file describes a library's types without its implementation. How TypeScript declaration files work, and how to write one for an untyped JS package.

#TypeScript #JavaScript #Web Development