TypeScript Structural Typing

Last updated Mar 10, 2022typescripttype-system

TypeScript uses structural typing, this means when comparing types, TypeScript only takes into account the members on the type. This means if the type is shaped like a duck, it’s a duck. If a goose has all the same attributes as a duck, then it also is a duck.

TypeScript’s structural typing was designed based on how JavaScript code is typically written. Because JavaScript widely uses anonymous objects like function expressions and object literals, it’s much more natural to represent the kinds of relationships found in JavaScript libraries with a structural type system instead of a nominal one.

This is in contrast to nominal type systems — each type is unique and even if types have the same data you cannot assign across types.

If the object or class has all the required properties, TypeScript will say they match, regardless of the implementation details.

interface Ball {
  diameter: number
}
interface Sphere {
  diameter: number
}

let ball: Ball = {diameter: 10}
let sphere: Sphere = {diameter: 20}

sphere = ball // ✅ OK
ball = sphere // ✅ OK

The basic rule for TypeScript’s structural type system is that X is compatible with Y if Y has at least the same members as X. If we add in a type which structurally contains all of the members of Ball and Sphere, then it also can be set to be a ball or sphere.

interface Tube {
  diameter: number
  length: number
}

let tube: Tube = {diameter: 12, length: 3}

ball = tube // ✅ OK
tube = ball // 🔴 Error

Because a ball does not have a length, then it cannot be assigned to the tube variable. However, all of the members of Ball are inside tube, and so it can be assigned.

TypeScript is comparing each member in the type against each other to verify their equality. A function is an object in JavaScript and it is compared in a similar fashion. With one useful extra trick around the params:

let createBall = (diameter: number) => ({diameter})
let createSphere = (diameter: number, useInches: boolean) => {
  return {diameter: useInches ? diameter * 0.39 : diameter}
}

createSphere = createBall // ✅ OK
createBall = createSphere // 🔴 Error

When comparing two objects of a class type, only members of the instance are compared. Static members and constructors do not affect compatibility.

class Animal {
  feet: number
  constructor(name: string, numFeet: number) {}
}
class Size {
  feet: number
  constructor(numFeet: number) {}
}
let a: Animal
let s: Size

a = s // ✅ OK
s = a // ✅ OK

This can have some surprising consequences for programmers accustomed to working in a nominally-typed language. For example there are cases where a string or number can have special context and you don’t want to ever make the values transferrable.

  • User Input Strings (unsafe)
  • Translation Strings
  • User Identification Numbers
  • Access Tokens

You may have heard duck typing — if it walks like a duck and it quacks like a duck, then it’s a duck. Structural typing and duck typing are very similar — structural typing is often used to refer to such checks run at compile time, while duck typing refers to the same checks run at runtime.

Structural typing is a necessity for TypeScript to be compatible with JavaScript’s duck typing and is a key part of how Typescript allows types to be added gradually to an untyped code base.