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.
4 min read
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" PointVector 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.
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.
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 valueStructural 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.)
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 checkThis 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.
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.
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.
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?
TypeScript