Structural Typing
Reviewed & published by Brayan K
Structural typing means TypeScript decides whether two types are compatible by comparing their shape — the properties and methods they have — rather than their declared name, so any object with the required members counts as that type.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Why TypeScript matches types by shape, not by name
- How 'duck typing' lets undeclared objects satisfy a type
- Why excess property checks fire only on fresh object literals
- How to dodge an excess-property error via a variable
- The rule of assignability: 'has at least the required members'
- Why two unrelated named types can be fully compatible
💡 Real-World Analogy
A power socket doesn't ask which company made your charger — it only checks that the plug has the right prongs. Any device with a matching plug "fits," regardless of brand or name. Structural typing is the same: TypeScript checks the prongs (the properties), not the label on the box. If the shape fits, it plugs in.
1. Shape Over Name
A function that needs a Point with x and y will accept any object that has those properties — even one you never declared as a Point. There's no implements, no inheritance, no conversion: having the right structure is being the type. This is why TypeScript is described as "structurally typed" and nicknamed "duck typed."
interface Point { x: number; y: number }
function distance(p: Point): number {
return Math.hypot(p.x, p.y);
}
// 'vector' was NEVER declared as a Point — but it has the right shape,
// so structural typing accepts it. No 'implements', no conversion:
const vector = { x: 6, y: 8, label: "v" };
distance(vector); // ✅ has x and y, so it IS a Point structurally
// Contrast with nominal languages (Java/C#) where you'd need the value to be
// declared 'a Point' by name. TypeScript only cares about the structure.// STRUCTURAL TYPING: TypeScript matches types by their SHAPE, not their name.
// If an object has the right properties, it "is" that type — even if it was
// never declared to be. (Also nicknamed "duck typing": walks/quacks like a duck.)
// In TS: interface Point { x: number; y: number }
function distance(p) {
// We only need .x and .y — ANY object with those works:
return Math.sqrt(p.x * p.x + p.y * p.y);
}
const point = { x: 3, y: 4 };
console.log(distance(point)); // 5
// This object was never called a "Point", yet it has the right SHAPE,
// so in TS it's accepted as a Point with no conversion:
const vector = { x: 6, y: 8, label: "v" }; // extra prop is fine here
console.log(distance(vector)); // 10
// The name of the type is irrelevant — only the members matter.2. Excess Property Checks
Structural typing normally allows extra properties — a wider object fits a narrower requirement. But TypeScript makes one strict exception: when you assign a fresh object literal directly to a typed target, an unexpected property is flagged. That's because a literal with a surprise property is almost always a typo. Route the literal through a variable first and the special check disappears, leaving plain assignability.
interface Point { x: number; y: number }
// ❌ Fresh object literals get an EXTRA strictness check:
const a: Point = { x: 1, y: 2, z: 3 };
// Object literal may only specify known properties, and 'z' does not exist...
// ✅ But assign through a variable and the excess check is skipped —
// plain structural assignability takes over (extra props allowed):
const tmp = { x: 1, y: 2, z: 3 };
const b: Point = tmp; // OK — tmp is a superset of Point
// The literal-only check is a deliberate "typo catcher" at the literal site.// EXCESS PROPERTY CHECKS: TS is normally happy with extra properties (as we
// saw), BUT it makes a special exception for OBJECT LITERALS assigned directly.
// A fresh literal with an unexpected property is flagged — likely a typo.
// In TS: const p: Point = { x: 1, y: 2, z: 3 }; // ❌ 'z' is excess
// At runtime there's no such check — JS just keeps the extra property:
const direct = { x: 1, y: 2, z: 3 };
console.log(direct.z); // 3 (JS doesn't care)
// The check only fires on a FRESH literal. Assign via a variable first and
// the extra property is allowed (structural rules take over):
const tmp = { x: 1, y: 2, z: 3 };
const widened = tmp; // in TS, assigning Point = tmp is fine — no excess check
console.log(widened.x, widened.y); // 1 2
// The excess-property check exists to catch typos like 'colour' vs 'color'
// at the exact spot you write the literal.3. Assignability: Has at Least What's Required
The core rule is simple: a value is assignable to a type when it has at least every member that type requires. A wider object can stand in for a narrower one, and the extra fields are simply ignored. A direct consequence is that two named types you never related to each other — a Person and a Product — can both satisfy a function that needs only their common shape.
// Two unrelated types with the same shape are interchangeable:
type Person = { name: string; age: number };
type Product = { name: string; price: number };
function announce(thing: { name: string }): string {
return "Here is " + thing.name;
}
const ada: Person = { name: "Ada", age: 36 };
const mug: Product = { name: "Mug", price: 9 };
announce(ada); // ✅
announce(mug); // ✅ — both have 'name', so both fit { name: string }
// "Has at least the required members" = assignable. A wider object is
// assignable to a narrower required type; the extras are simply ignored.// ASSIGNABILITY: a value of type A is assignable to type B if A has (at least)
// everything B requires. More properties = MORE specific = still assignable to
// the type that asks for fewer. Two UNRELATED named types can be compatible
// purely because their shapes line up.
// In TS: type Named = { name: string }; type Labelled = { name: string };
function announce(thing) {
return "Here is " + thing.name;
}
// A 'Person' and a 'Product' were never related, but both have .name,
// so both satisfy the { name: string } shape 'announce' needs:
const person = { name: "Ada", age: 36 };
const product = { name: "Mug", price: 9 };
console.log(announce(person)); // Here is Ada
console.log(announce(product)); // Here is Mug
// A wider object is assignable to a narrower required shape:
function area(rect) { return rect.width * rect.height; }
const box = { width: 2, height: 3, depth: 4, label: "box" };
console.log(area(box)); // 6 (extra depth/label simply ignored)🎯 Your Turn
A robot and a pet share nothing but a name. A function needing only { name: string } accepts both. Fill in the blank marked ___ with the shared property, then run it.
// 🎯 YOUR TURN — replace the ___ then press "Try it Yourself".
// Both objects have different extra fields, but BOTH have a 'name'.
// 'greet' only needs the shape { name: string } — fill in the property it reads.
function greet(thing) {
return "Hello, " + thing.___; // 👉 the shared property: name
}
const robot = { name: "R2", model: "astromech" };
const pet = { name: "Rex", species: "dog" };
console.log(greet(robot)); // Hello, R2
console.log(greet(pet)); // Hello, Rex
// ✅ Expected output:
// Hello, R2
// Hello, RexCommon Errors (and the fix)
- ❌ "Object literal may only specify known properties, and 'z' does not exist..." — an excess property on a fresh literal. ✅ Fix the typo, or widen the type/add an index signature if the field is intended.
- ❌ Expecting nominal behaviour — assuming an object must be "declared a Point" to count. ✅ In TypeScript, the right shape is enough; no declaration needed.
- ❌ Silencing excess errors with as — hides a real typo. ✅ Prefer fixing the property name; reserve assertions for genuine cases.
- ❌ Assuming extra props are forbidden everywhere — they're only checked on fresh literals. ✅ Through a variable, supersets are accepted by assignability.
Pro Tips
- 💡 Type parameters by the shape you need, not the biggest type available — accept { id: string } if that's all you use, and callers get more flexibility.
- 💡 Excess-property errors are friends — they catch real typos at the literal site. Read the property name it complains about before reaching for a workaround.
- 💡 If you need true nominal typing (so two same-shaped types can't mix), use branded types — covered later in this course.
- 💡 Functions are compared structurally too — parameter and return types must be compatible, which enables safe callback substitution.
📚 Keep learning
- TypeScript Handbook — Type Compatibility
- TypeScript Handbook — Excess Property Checks
- TypeScript Handbook — Structural relationships between classes
Mini-Challenge: Sum by Shared Field
No blanks this time — just a brief and a starting outline. Build the sum yourself, run it, and check your output against the example in the comments.
// 🎯 MINI-CHALLENGE: a function that only cares about ONE field.
// Structural typing means totalPrice can accept ANY array of objects, as long
// as each one has a numeric 'price' — the other fields differ wildly.
//
// Write totalPrice(items) that:
// 1. Sums up every item's .price (use reduce, starting at 0).
// 2. Works even though the items have different extra properties.
//
// Use the 'cart' below — note each item has DIFFERENT other fields.
//
// ✅ Expected output:
// 10
const cart = [
{ name: "Pen", price: 2, color: "blue" },
{ name: "Pad", price: 5 },
{ id: 99, price: 3, inStock: true },
];
// your code here🎉 Lesson Complete
- ✅ TypeScript compares types by shape, not by name (structural typing)
- ✅ Any object with the required members satisfies a type — "duck typing"
- ✅ Excess property checks fire only on fresh object literals
- ✅ Routing a literal through a variable skips the excess check
- ✅ Assignability = "has at least the required members"; extras are ignored
Practice quiz
What does structural typing compare to decide compatibility?
- The declared type name
- The file the type lives in
- The shape - the set of properties and methods
- The order types were declared
Answer: The shape - the set of properties and methods. TypeScript matches types by structure, not by their declared name.
Why is structural typing nicknamed 'duck typing'?
- If it has the right members, it counts as the type - walks/quacks like a duck
- It involves real ducks
- It only works on animals
- It quacks at runtime
Answer: If it has the right members, it counts as the type - walks/quacks like a duck. TypeScript asks what a value can do, not what it is called.
Does an object need to be declared 'a Point' to be used as one?
- Yes, with implements
- Yes, with a class
- Only with as Point
- No - having x and y is enough
Answer: No - having x and y is enough. Any object with the required members satisfies the type; no declaration needed.
When does an excess property check fire?
- On every object
- On a fresh object literal assigned directly to a typed target
- Only inside functions
- Never in strict mode
Answer: On a fresh object literal assigned directly to a typed target. TypeScript flags extra properties only on fresh literals - a deliberate typo catcher.
How can you make an excess-property error go away (legitimately)?
- Assign the literal to a variable first, then assign that variable
- Use any
- Delete the type
- Add @ts-ignore
Answer: Assign the literal to a variable first, then assign that variable. Routing through a variable skips the literal-only check; plain assignability takes over.
What is the core rule of assignability?
- Types must share a name
- Both types need the same number of properties
- A value is assignable if it has at least every member the type requires
- Objects must be frozen
Answer: A value is assignable if it has at least every member the type requires. Has at least the required members = assignable; extras are ignored.
Can a wider object stand in for a narrower required shape?
- No
- Yes - the extra properties are simply ignored
- Only with a cast
- Only if frozen
Answer: Yes - the extra properties are simply ignored. A superset object satisfies a smaller required shape; extra fields don't matter.
Can two unrelated named types be compatible?
- No, names must match
- Only if one extends the other
- Only for classes
- Yes, if their shapes line up (e.g. both have name: string)
Answer: Yes, if their shapes line up (e.g. both have name: string). Structural compatibility means coincidental matching shapes are interchangeable.
How does TypeScript's typing differ from Java/C# nominal typing?
- It is identical
- Nominal typing needs explicit declaration by name; structural only needs the shape
- Structural typing is slower
- Nominal typing ignores names
Answer: Nominal typing needs explicit declaration by name; structural only needs the shape. Nominal systems require declaring a value to be a type by name; TypeScript only checks structure.
If you need two same-shaped types to be incompatible, you should use:
- as const
- Object.freeze
- branded (nominal) types
- an index signature
Answer: branded (nominal) types. Branded types add a unique marker so structurally identical types can't mix.
Continue this course
- Previous: void, null & undefined
- Next: type vs interface — When to use each — merging, extends, unions, and style guidance
- Quick reference: TypeScript cheat sheet
Frequently asked questions
What is structural typing in plain terms?
TypeScript decides whether two types are compatible by comparing their structure — the set of properties and methods they have — rather than their declared name. If an object has all the members a type requires, it counts as that type, even if it was never explicitly declared to be. This is the opposite of nominal typing (used by Java and C#), where compatibility depends on the type's name and explicit declarations like 'implements'.
Why is it also called duck typing?
From the saying 'if it walks like a duck and quacks like a duck, it's a duck.' TypeScript doesn't ask what a value is called; it asks what it can do. An object with x and y is treated as a Point because it has the shape of one. Duck typing is the dynamic-language version of this idea; TypeScript brings the same spirit to compile-time checking.
If extra properties are allowed, why did TypeScript reject my object literal?
Because of excess property checks. TypeScript normally permits a value to have more properties than required, but it makes a special exception for fresh object literals assigned directly to a typed target. There, an unexpected property is almost always a typo (like 'colour' instead of 'color'), so it's flagged. Assigning the literal to a variable first, or widening the target type, skips that extra check.
How can two unrelated types be compatible?
Because only their shapes are compared. A Person { name; age } and a Product { name; price } are unrelated by name, but a function needing just { name: string } accepts both — each has the required 'name' property. This is structural assignability: 'has at least the required members' is the only test, so coincidental shape matches are intentionally allowed.