Structural Typing vs. Nominal Typing: TypeScript and Java Compared
Same interface, two unrelated types, two completely different rules about what counts as a match.
Imagine an interface called Movable, with nothing more exotic than an x coordinate, a y coordinate, and a move operation. Now imagine two classes that have never heard of each other: a Drone and a BoardGamePiece. Both happen to expose an x, a y, and a move method. In TypeScript, that coincidence is enough; both classes satisfy Movable without a single line announcing it. In Java, that same coincidence means nothing at all until one of the classes explicitly writes the words “implements Movable.” That one difference, whether the compiler trusts a declared name or the actual shape of the data, is the entire dividing line between nominal and structural typing, and it quietly shapes how comfortable refactoring feels in each language.
Nominal Typing: Java Trusts the Label, Not the Shape
Java’s type system is nominal, meaning type compatibility is decided by declared relationships, not by comparing members side by side. A class is only considered an instance of an interface if it, or one of its ancestors, explicitly says so with an implements or extends clause. This is spelled out plainly in the Java Language Specification’s chapter on classes and the companion chapter on interfaces: subtyping is a relation the programmer declares, and the compiler simply checks that the declaration holds.
Going back to the running example, the Drone class does not accidentally satisfy Movable in Java just because it has matching fields and a matching method signature. Unless the class declaration reads “class Drone implements Movable,” the compiler treats Drone and Movable as entirely unrelated types, even if every member lines up perfectly. That might feel rigid, but it buys something real: a Java interface functions as a promise the author made on purpose, not a pattern the compiler noticed by accident.
What this costs you
The price of nominal typing shows up whenever you want to unify two pre-existing classes under a shared abstraction after the fact. If Drone and BoardGamePiece were written independently and neither declares Movable, no code anywhere can treat them interchangeably as Movable, no matter how identical their shapes are, until someone edits at least one of the class declarations or introduces an adapter. Retrofitting a shared interface onto existing code is therefore always an explicit, visible change in Java, never a silent one.
Structural Typing: TypeScript Trusts the Shape
TypeScript flips that logic entirely, formalizing the duck-typing habit JavaScript developers already had. Its type system, described in detail in the TypeScript Handbook’s chapter on object types, decides compatibility by comparing the members two types expose, not by checking whether either type declares a relationship to the other. If Drone has an x, a y, and a move method with a compatible signature, TypeScript considers it assignable to Movable automatically, even if the word Movable never appears anywhere near the Drone class.
This is precisely what most people mean when they say TypeScript formalizes duck typing: if it walks like a duck and quacks like a duck, TypeScript is satisfied, regardless of whether anything ever called it a duck. For the running example, both Drone and BoardGamePiece slot into any function that expects a Movable without either class needing to know Movable exists.
The one place structural typing gets stricter: excess property checks
Structural typing has one deliberate wrinkle worth knowing, because it trips people up constantly. When you hand TypeScript a freshly written object literal directly, rather than a variable, it performs what the Handbook calls an excess property check and will flag extra properties that were not part of the target shape. Assign that same literal to a variable first, and the extra properties are suddenly fine. The underlying compatibility rule never changes; TypeScript is simply guessing that a literal typo is more likely to be a mistake than an intentional widening, so it tightens the check at exactly the moment you are least likely to have meant it.
Side by Side: The Same Question, Answered Differently
| Aspect | Java (Nominal) | TypeScript (Structural) |
|---|---|---|
| Basis for compatibility | Declared relationship (implements/extends) | Matching members and signatures |
| Two unrelated classes with identical shapes | Not interchangeable unless both declare the interface | Interchangeable automatically |
| Retrofitting a shared interface onto old code | Requires editing the class declaration | Requires nothing; compatibility already exists |
| Risk of accidental compatibility | Very low; every match is intentional | Possible with coincidentally similar shapes |
| Object literal edge case | Not applicable | Excess property checks tighten literal assignment |
| Typical mental model | “What was this declared to be?” | “What does this actually look like right now?” |
Why the Difference Isn’t Just Academic
Nominal typing makes an interface feel like a contract with an author behind it. When a Java interface changes, every implementing class was, by definition, written with awareness that the interface existed, so the blast radius of a breaking change is at least discoverable through the type hierarchy. Structural typing removes that safety net but replaces it with something JavaScript developers already valued: the ability to write a function once and have it accept anything close enough in shape, without asking any existing code to change or even to know the function exists.
The trade-off becomes concrete in large codebases. A Java team introducing a new interface across dozens of pre-existing classes has to touch every one of those class declarations, a mechanical but visible refactor. A TypeScript team introducing the equivalent interface touches nothing except the new function signature; every object that already has the right shape starts satisfying it immediately, for better and for worse. That same convenience is also why TypeScript, despite compiling to loosely typed JavaScript underneath, still manages to catch a large share of the shape mismatches that historically caused runtime errors in plain JavaScript, and why it climbed from twelfth place in the 2018 Stack Overflow survey’s usage rankings to the top five within a few years, a rise The Register covered in its writeup of the 2022 results.
What the Developer Data Shows
It helps to put actual numbers next to the theory. The 2024 Stack Overflow Developer Survey asked the same two questions of both languages: how many developers used each one extensively over the past year, and, of those, how many want to keep using it.
Extensive Use Over the Past Year, 2024

TypeScript’s structural checks apparently do not slow teams down enough to dent adoption; it is used more extensively than Java among 2024 respondents, a notable fact given that Java has decades of enterprise entrenchment behind it and TypeScript is a comparatively recent addition to most stacks.
Admired Score by Language, 2024

The admiration gap is even wider than the usage gap. Structural typing’s flexibility, plus the safety net TypeScript adds on top of plain JavaScript, seems to translate directly into developers wanting to keep working with it, while Java’s admiration score trails noticeably despite its far longer track record and its enormous existing install base.
Quick Checklist: Spotting Which Typing Model You’re Reasoning About
- Ask whether a matching shape alone is enough, or whether a declared relationship is required first.
- Check whether adding a new shared interface to old code means editing the old code or means nothing at all.
- Watch for TypeScript’s excess property check specifically around freshly written object literals, since it is the one place structural rules briefly act nominal.
- Remember that nominal typing makes breaking changes discoverable through declared relationships, while structural typing makes them discoverable only by re-checking every shape that happens to match.
- When retrofitting an abstraction onto existing classes, expect a mechanical edit in Java and expect nothing at all in TypeScript.
What We Have Learned
Java and TypeScript answer the exact same question, “does this type satisfy that interface,” in opposite ways. Java checks a declared relationship and ignores shape entirely, which means an interface is always an intentional, discoverable contract, at the cost of needing explicit edits whenever old code needs to join a new abstraction. TypeScript checks the shape directly and ignores declared relationships, formalizing the duck typing JavaScript developers already relied on, with the one notable exception of excess property checks on fresh object literals.
Neither model is a mistake the other language failed to make; each is a deliberate bet about whether safety should come from declared intent or from flexible structural matching. The 2024 developer survey data suggests that bet is paying off for TypeScript specifically, since it now sees both higher extensive usage and a meaningfully higher admiration score than Java, even though Java’s nominal contracts remain the safer choice for large, long-lived interface hierarchies where discoverability matters most.

