Skip to content
Theory · Interfaces and structural typing · lesson 5 of 12

Structural typing

Back to the lock. A locksmith made two keys with identical teeth: one has "Point" stamped on its head, the other "Vec". Both open the lock. It does not read what is stamped on the head; all it cares about is the teeth.

TypeScript reasons the same way. Say a program has a point on a map, Point, and a shift, Vec. They mean different things, but they are built the same: both hold numbers x and y. Let's pass a shift where a point is expected:

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

function show(p: Point): string {
    return `(${p.x}, ${p.y})`;
}

const shift: Vec = { x: 3, y: 4 };
console.log(show(shift)); // (3, 4)

Let's go through the example line by line:

  1. Point and Vec are two different interfaces with the same members.
  2. The function show expects a Point.
  3. The constant shift is declared as a Vec, and nothing says it is a Point.
  4. The call show(shift) passes anyway: shift has x and y, both numbers — exactly what Point requires. The program prints (3, 4).

Shape, not name

This way of comparing types is called structural typing: a value fits a type if it has every required property with the required type. What the value's type is called and where it was declared does not matter.

Many languages, such as Java and C#, follow a different rule — nominal typing. There a Vec would fit where a Point is expected only if the declaration of Vec said explicitly that it is a Point — for example, with extends. Having the same properties is not enough. Interviewers often ask how structural typing differs from nominal typing.

Structural typing has a downside. The compiler cannot tell Point from Vec, and if you pass a shift instead of a point by mistake, it stays silent. You have to tell such values apart yourself, for example by descriptive variable names.

Extra properties do not get in the way

A value may have more properties than the type requires. A key ring on a key does not bother the lock:

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

function show(p: Point): string {
    return `(${p.x}, ${p.y})`;
}

const pin = { x: 1, y: 2, label: "home" };
console.log(show(pin)); // (1, 2)

pin has the type { x: number; y: number; label: string; }: there is no name, only a shape. It has everything Point requires, and the extra label does not get in the way. The program prints (1, 2).

A literal on the spot is checked more strictly

There is one exception. If an object literal is written right where a Point is expected, tsc checks it for extra properties too. Nobody else sees that object, so an extra property in it is almost certainly a typo. Step through the program and compare the line where the extra property came from a variable with the line where it sits in a literal:

LiveWhat fits Pointstep 1 of 6

The constant shift is declared with the type Vec.

Press "Next step": the hover shows a type or an error on the highlighted line

Here the lock analogy stops working. A real lock does not care where the key came from, while TypeScript is stricter with a literal written on the spot than with a value from a variable.

Common mistake

Expecting the parameter's type to cut off extra properties:

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

function save(p: Point): string {
    return JSON.stringify(p);
}

const pin = { x: 1, y: 2, label: "home" };
console.log(save(pin)); // ✗

The compiler stays silent, and the program prints {"x":1,"y":2,"label":"home"}, not {"x":1,"y":2}. The type Point only allows reading x and y; it does not change the object itself: types are erased, and at run time p holds the same object with label. If only the coordinates should go out, build a new object: { x: p.x, y: p.y }.

Key points

  • TypeScript compares types by shape: a value fits if it has every required property with the required type, and the type's name does not matter.
  • That is why a Vec fits where a Point is expected, and the compiler cannot tell one from the other.
  • Extra properties on a value from a variable do no harm; in a literal written on the spot they are error TS2353.
  • A type does not remove extra properties from an object: at run time the object stays as it was.