TypeScript path · The compiler · Topic 2 of 11

Structural typing

Not started

TypeScript decides whether a value fits a type by looking at its shape: the members it has and their types. It does not care what the value's type was called or where it was declared. This matches how JavaScript code actually uses objects (if it has a name, you can read the name), but it also means that types which happen to share a shape are interchangeable, whether you meant them to be or not.

Why it matters: It is why a plain object can stand in for a class, why two unrelated types can be mixed up by accident, and why extra properties are sometimes an error and sometimes not.

  1. 1Compatibility is about shape

    A value fits a type if it has at least the required members, with compatible types. Where the type was declared, and what it is called, does not matter.

  2. 2Fresh object literals are stricter

    An object literal written directly where a type is expected may not have unknown properties: that is almost always a typo. The same object passed through a variable is fine.

  3. 3Brands make names matter

    When two types share a shape but must not be mixed, such as user ids and order ids, add a phantom brand property that no plain value has.

1

How it works

The explanation, with examples you can run.

Shape, not name

interface Named {  name: string} class Person {  name: string  age: number  constructor(name: string, age: number) {    this.name = name    this.age = age  }} const fromClass: Named = new Person("Ada", 36) // fine: it has a nameconst fromLiteral: Person = { name: "Lin", age: 30 } // fine: same shape as Person

Person never mentions Named, and the object literal was never created with new Person, yet both assignments compile. There is no implements needed, because types are erased and the checker can only compare structure. One consequence: fromLiteral instanceof Person is false at runtime, even though the checker accepted it as a Person. Classes become nominal only when they have private or #private members, which a plain object cannot copy.

The excess property check

type Point = { x: number; y: number } const p3 = { x: 1, y: 2, z: 3 }const ok: Point = p3 // fine: extra members are allowed const typo: Point = { x: 1, y: 2, zz: 3 } // Error: Object literal may only specify known properties

Structurally both are the same assignment. The difference is freshness: an object literal written directly where a Point is expected cannot be used for anything else, so an unknown property in it is almost certainly a mistake. Once the object is in a variable, it may be used elsewhere with its extra members, so the check does not apply. This is a targeted lint, not a guarantee that values have no extra properties.

When the shape is not enough

Ids are the classic case. A user id and an order id are both strings, and a function that takes (userId: string, orderId: string) will happily accept them in the wrong order. A brand adds a property that exists only in the type:

type UserId = string & { readonly __brand: "UserId" }type OrderId = string & { readonly __brand: "OrderId" } const asUserId = (raw: string) => raw as UserId function loadOrder(user: UserId, order: OrderId) {  return `${user}/${order}`} const u = asUserId("u_1")const o = "o_9" as OrderIdloadOrder(o, u) // Error: OrderId is not assignable to UserId

At runtime they are still plain strings. The brand only exists for the checker, so create branded values in one place (the function that parses or validates them) and let the types carry the guarantee from there.

Functions are compared structurally too

A function that takes fewer parameters can be passed where one taking more is expected, which is why [1, 2].forEach((n) => ...) works even though forEach passes three arguments. Under strictFunctionTypes, parameter types are checked so that a callback cannot demand a narrower argument than it will be given.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.`type Point = { x: number; y: number }`. Does `const p3 = { x: 1, y: 2, z: 3 }; const p: Point = p3` compile?
  2. 2.And `const p: Point = { x: 1, y: 2, z: 3 }`?
  3. 3.`type UserId = string` and `type OrderId = string`. Can you pass an `OrderId` where a `UserId` is expected?

0 of 3 right so far. Get all three to finish this topic.