CodeOath
← All posts
TypeScript75 min total · 21 parts

TypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime

Contents — Part 4 of 21: Structural Typing: TypeScript Checks Shape, Not Names
Part 4 of 21 · ~1 min

Structural Typing: TypeScript Checks Shape, Not Names

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

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

const location = { x: 3, y: 4, label: "home" };
printPoint(location); // valid — location has at least x and y as numbers

location was never declared as a Point — TypeScript doesn't care. This is structural typing ("duck typing," checked at compile time): if a value has the right shape, it satisfies the type, regardless of name or explicit declaration. This is a real difference from languages like C# or Java, where a type has to be nominally declared as implementing an interface to satisfy it — see C# Fundamentals for the nominal side of that comparison.

Structural typing has a specific, commonly-tested gotcha with object literals: excess property checking.

interface Config { url: string; }

function connect(config: Config) { /* ... */ }

connect({ url: "https://api.example.com", timeout: 5000 });
// Error: Object literal may only specify known properties, and 'timeout'
// does not exist in type 'Config'.

const options = { url: "https://api.example.com", timeout: 5000 };
connect(options); // fine — no error, because `options` isn't a fresh literal

Both calls pass a value with an "excess" timeout property, and structurally, both should be fine — extra properties don't violate structural compatibility. But TypeScript special-cases object literals assigned or passed directly with extra checking, on the theory that a literal with a property the target type doesn't recognize is more likely a typo than an intentional excess field. Assigning it to a variable first (as options does) sidesteps that extra check, because a variable of inferred type is no longer a "fresh" literal at the call site.