TypeScript

Structural typing — why TypeScript checks shape, not name

Two unrelated types with identical fields are interchangeable in TypeScript, even if they were never declared to have anything to do with each other. This single design decision explains a lot of behavior that looks surprising coming from a nominally-typed language.

Beginner

4 min read

The surprising part, shown directly

interface Point {
  x: number;
  y: number;
}
 
class Vector {
  constructor(public x: number, public y: number) {}
}
 
function printPoint(p: Point) {
  console.log(`(${p.x}, ${p.y})`);
}
 
printPoint(new Vector(3, 4)); // works fine — Vector is never declared to "implement" Point

Vector never says implements Point anywhere. In a nominally-typed language (Java, C#), this would be a compile error — a Vector is a Vector, not a Point, regardless of what fields it happens to have. TypeScript compiles this without complaint, because it checks structure, not declared identity: printPoint needs an object with numeric x and y fields, Vector instances have exactly that, so a Vector satisfies Point — the two types were never related by name, only by shape.

"If it has the fields, it fits" — the actual rule

The rule is genuinely simple once stated directly: any value with at least the required fields, of the required types, satisfies an interface or type — regardless of what class it was constructed from, or whether it came from a class at all. A plain object literal { x: 1, y: 2 } satisfies Point exactly as well as a real Vector instance does. This is called structural typing (sometimes "duck typing," borrowed from the older dynamic-language idiom "if it walks like a duck and quacks like a duck") — TypeScript's entire type system is built on this idea, not the nominal, name-based typing most mainstream statically-typed languages use.

Extra fields are fine too — this is not an exact match

interface Point {
  x: number;
  y: number;
}
 
const p = { x: 1, y: 2, z: 3, label: "origin" };
printPoint(p); // still works — extra fields don't disqualify a value

Structural compatibility only requires the minimum shape — a value can have additional fields beyond what's required and still satisfy the type. This matters in practice constantly: a database row with fifteen columns still satisfies a function that only needs three of them, with no need to strip the object down first. (There's one sharp-edged exception worth knowing about, covered next.)

Excess property checks: the one case where TypeScript IS stricter

function printPoint(p: { x: number; y: number }) {
  console.log(p);
}
 
printPoint({ x: 1, y: 2, z: 3 }); // Error: Object literal may only specify known properties
                                    // — 'z' does not exist in type '{ x: number; y: number; }'
 
const obj = { x: 1, y: 2, z: 3 };
printPoint(obj); // fine — no error, same extra field, different check

This looks like it contradicts "extra fields are fine" — it doesn't, but the distinction is subtle and catches nearly everyone at least once. Object literals passed directly as an argument get an extra check TypeScript calls "excess property checking," specifically because a freshly-written object literal with a field the target type doesn't recognize is very often a typo (lable instead of label) that structural typing alone would silently let through. Assigning that same literal to a variable first (const obj = {...}), then passing the variable, sidesteps this extra check — at that point it's an ordinary value being checked structurally, not a literal being checked for excess properties.

Why interfaces don't need implements at all

// No relationship declared anywhere between these two types —
// and none is needed for the function below to accept both
interface Logger {
  log(message: string): void;
}
 
const consoleLogger: Logger = { log: (msg) => console.log(msg) };
const fileLogger: Logger = { log: (msg) => appendToFile(msg) };
 
function process(logger: Logger) {
  logger.log("processing...");
}

Because compatibility is checked by shape, not declared relationship, a plain object literal can satisfy an interface just by having the right methods — no class hierarchy required at all. This is the structural-typing foundation the classes lesson's implements keyword sits on top of: implements is a genuinely useful, explicit statement of intent (and a way to get a compile error immediately if the class doesn't actually match), but it's a convenience layered on structural typing, not what makes the compatibility check itself work.

Where this actually matters in real code

Structural typing is why mocking in tests is so lightweight in TypeScript — a test double just needs to have the right shape, not inherit from any real class or be declared as implementing anything. It's also why third-party library types often work seamlessly with your own domain types with zero adapter code: if the shapes line up, TypeScript accepts it, full stop. The trade-off is the excess-property-check sharp edge above, and a general need to think in terms of "what shape does this actually need" rather than "what class is this" — a mental shift that mostly rewards people who've worked in dynamically-typed JavaScript already, since it's much closer to how JavaScript objects behaved all along.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why does a `Vector` class instance satisfy a `Point` interface in TypeScript, even though `Vector` never declares `implements Point`?

2. Does having EXTRA fields beyond what a type requires disqualify a value from satisfying that type?

3. Why does `printPoint({ x: 1, y: 2, z: 3 })` error, but assigning that same object to a variable first and passing the variable does not?