Understanding TypeScript's Structural Type System vs. Nominal Typing Errors

Learn the key differences between TypeScript's structural type system and nominal typing, with a focus on common errors and how to fix them.

TypeScript uses a structural type system, which means compatibility between types is based on the shape or structure of the data, rather than its explicit name or declaration. This contrasts with nominal typing used in some other languages, where type compatibility depends on explicit declarations or names.

In a structural type system, if two types have the same properties and types, they are considered compatible, even if they are declared separately. This can sometimes cause confusion for beginners coming from nominally typed languages, especially when expecting type errors that do not occur, or when nominal typing would catch certain mistakes.

Here's an example illustrating structural typing in TypeScript:

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

interface Coordinate {
  x: number;
  y: number;
}

function logPoint(p: Point) {
  console.log(`${p.x}, ${p.y}`);
}

const coord: Coordinate = { x: 10, y: 20 };
logPoint(coord); // Works because coord and Point have the same structure

Even though `Point` and `Coordinate` are different interfaces, TypeScript allows passing a `Coordinate` to a function expecting a `Point` because their structures match.

In contrast, nominal typing would require explicit inheritance or declaration to treat these two types as compatible.

Sometimes, developers want nominal typing to prevent mixing different types with the same structure. TypeScript doesn’t have built-in nominal typing, but you can simulate it using techniques such as "branding":

typescript
// Simulating nominal typing using branding
type UserId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };

function createUserId(id: string): UserId {
  return id as UserId;
}

function createOrderId(id: string): OrderId {
  return id as OrderId;
}

function getUser(id: UserId) {
  console.log(`User ID: ${id}`);
}

const userId = createUserId("abc123");
const orderId = createOrderId("xyz789");

getUser(userId); // Works
// getUser(orderId); // Error: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'.

By adding a unique "brand" property with `unique symbol`, you help TypeScript distinguish types that are structurally identical but logically different, preventing mixing them up and catching errors.

To summarize:

- TypeScript uses structural typing, meaning types are compatible if their shapes match. - This enables flexibility but can cause confusion if you expect nominal type checks. - To simulate nominal typing, you can use branding techniques to make types incompatible unless explicitly converted. - Understanding this difference helps you avoid type errors and design more robust TypeScript programs.

With this knowledge, you'll better understand TypeScript errors related to type compatibility and how to design types that reflect your intended logic.