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

💡 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, Rex

Common Errors (and the fix)

Pro Tips

📚 Keep learning

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

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

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.