ProgrammingTypeScript Architect

What is the difference between structural and nominal typing in TypeScript? Can nominal typing be implemented, and if so, how? What problems can it solve?

Pass interviews with Hintsage AI assistant

Answer

TypeScript uses structural typing, also known as "duck typing". For type compatibility, the structure (signature) is important, not the name or origin of the type.

Example:

interface Point2D { x: number; y: number; } interface Coord2D { x: number; y: number; } // These types are interchangeable: Point2D and Coord2D, because the structure is the same. const foo: Point2D = { x: 1, y: 2 }; const bar: Coord2D = foo; // OK!

Nominal typing: for type compatibility, the "name" or "factory" is important, the structure is not.

In TypeScript, nominal typing is not natively supported, but it can be emulated using branded types:

type USD = number & { readonly __brand: unique symbol } type EUR = number & { readonly __brand: unique symbol } let priceUSD: USD; let priceEUR: EUR; // priceUSD = priceEUR; // Error! Different brands.

Why apply this? For example, to distinguish between structurally identical, but semantically different types — currencies, userID/tokenID, physical quantities, etc.


Trick Question

Question: Why does the following code compile without errors, even though Address and UserId are logically different types?

interface Address { value: string; } interface UserId { value: string; } let id: UserId = { value: "test" }; let addr: Address = id; // OK

Answer: Because in TypeScript, the structure is important, not the type name. Both types are just "an object with value: string".


Examples of real errors due to ignorance of the topic nuances


History

Project: A financial system with calculations in USD/EUR. Amounts were passed as numbers. Once, the currencies were mixed up during addition — due to structural typing, TypeScript did not catch it. Later, branded types were introduced to exclude such errors at compile time.


History

Project: In the development of a REST API, objects were used for IDs of different entities (userId, groupId), both with a value: string field. By mistake, userId was substituted instead of groupId, and only the business logic on the server detected the error.


History

Project: In a parsing library for DSL, identical structures were used (type value = { kind: 'num'|'str', value: number|string }). Homogeneous structures got mixed between different parts of the code, leading to logical errors. Artificial brand fields were added for separation.