Structural Typing: Why TypeScript Cares About Shape, Not Names

October 5, 2026 · 3 min read

In Java or C#, a function that asks for a Point wants an instance of the class Point (or something that explicitly declares itself one). In TypeScript, it wants anything shaped like a Point. Two types are compatible if their structure is compatible — names don't come into it.

This is called structural typing, and it's the reason TypeScript fits JavaScript so well.

type Point = { x: number; y: number }
function len(p: Point) { return Math.hypot(p.x, p.y) }
 
const a = { x: 3, y: 4 }
len(a)
const b = { x: 3, y: 4, z: 5 }
len(b)
len({ x: 3, y: 4, z: 5 })
class Vec { constructor(public x: number, public y: number) {} }
len(new Vec(1, 2))
len({ x: 3 })

len asks for a Point. In many languages that means "an instance declared as a Point". In TypeScript it means "anything with at least x: number and y: number". Types are compared by shape, not by name.

0 / 5

Shape is what counts

type Point = { x: number; y: number };

function len(p: Point) {
  return Math.hypot(p.x, p.y);
}

const a = { x: 3, y: 4 };
len(a);   // ✓ — a was never declared as a Point, but it has x and y

Classes are no different. A class that never mentions Point is accepted if its instances have the right properties:

class Vec {
  constructor(public x: number, public y: number) {}
}
len(new Vec(1, 2));   // ✓

Extra properties are fine — usually

A value with more than the type needs still fits:

const b = { x: 3, y: 4, z: 5 };
len(b);   // ✓ — b has everything a Point needs

That's why you can pass a rich object to a function that only needs a couple of its fields. The function's type describes what it uses, not everything the object might carry.

The exception: fresh object literals

len({ x: 3, y: 4, z: 5 });
// error: Object literal may only specify known properties,
//        and 'z' does not exist in type 'Point'.

Same shape as b, different result. When you write an object literal directly where a type is expected, TypeScript runs an extra excess property check: nothing else can ever see that z, so it's almost certainly a typo or a misunderstanding — like { colour: 'red' } for an option called color. It's a lint-style check layered on top of structural typing, applied only where it catches mistakes.

Missing properties are always errors

len({ x: 3 });
// error: Property 'y' is missing in type '{ x: number; }'
//        but required in type 'Point'.

Structural typing is generous about extras and strict about requirements.

Why TypeScript works this way

JavaScript code is written structurally already. Objects come from literals, JSON, spreads, and libraries, rarely from a declared class. A function that reads p.x and p.y genuinely works with anything that has them — "duck typing". Structural types describe that existing behaviour instead of forcing a class hierarchy onto it.

It also makes testing easy: a test can pass a small object literal with just the methods a function calls, without building the real dependency.

When you want names to matter: brands

Sometimes two types have the same shape but must not be mixed up:

type UserId = string;
type OrderId = string;

function cancelOrder(id: OrderId) { … }
const userId: UserId = 'u_42';
cancelOrder(userId);   // ✓ compiles — both are just strings

The usual workaround is a brand: an intersection with a property that never exists at runtime, so the two types differ structurally.

type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

const toOrderId = (s: string) => s as OrderId;

cancelOrder(userId);           // ✗ error now
cancelOrder(toOrderId('o_7')); // ✓

It's a compile-time-only label: at runtime both are plain strings.

The takeaway

In TypeScript, a type is a description of shape. If a value has the properties a type requires, it is that type — wherever it came from. Remember the two special cases: fresh object literals get checked for extras, and brands let you make names matter when shapes alone aren't enough.