Unlike many object-oriented languages, TypeScript implements structural typing (duck typing): an object is considered to match a type if it has all the necessary properties, regardless of whether it is explicitly declared as that type.
This flexibility can sometimes lead to unexpected acceptance of objects as a type if the structure matches, even though they are not meaningfully related. This is dangerous with complex data models where structure coincidentally aligns.
Always structure object types correctly, minimize structural matches of different entities, and for critical cases, use additional properties or symbols for "nominalization" of types.
Code example:
interface Point { x: number; y: number; } interface Pixel { x: number; y: number; } function drawPoint(p: Point) { console.log(p.x, p.y); } const pixel: Pixel = { x: 1, y: 2 }; drawPoint(pixel); // OK, types are structurally compatible
Key features:
If the structure of two interfaces matches, does it mean they are completely interchangeable?
Probably in structure, but not in program logic. This is acceptable at the compiler level but can lead to logical errors (for example, Point and Pixel above).
Is it possible to prohibit structural compatibility for a certain type?
Not completely, but you can add a unique property (for example, with a symbol):
interface Brand { _brand: unique symbol; }
Now another object cannot mimic this type without the same unique symbol.
How does structural typing differ from nominative?
Structural is based on the presence of structure, nominative is based on the matching name of the type within a certain namespace. In TS, structural typing is always the default.
In several entities, fields unintentionally matched (for example, User and Admin as {id: number, name: string}), leading to confusion when working with API contracts.
Pros:
Cons:
Using unique "tags" of symbols and non-standard fields to delineate semantically different types with the same structure.
Pros:
Cons: