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.
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 typing | Nominal typing | |
|---|---|---|
| Compatibility rule | Based on shape/members | Based on declared name/lineage |
| Example languages | TypeScript, Go, OCaml | Java, C#, Swift, Rust |
| Explicit relationships needed | No — shape match is enough | Yes — implements/extends or equivalent |
| Accidental compatibility | Possible — unrelated types can satisfy each other by coincidence | Not possible — must be declared |
| Refactoring safety | Renaming a type doesn’t break assignability elsewhere | Renaming 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.
Keep reading
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.
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.
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.