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 PersonPerson 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 propertiesStructurally 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 UserIdAt 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.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.