How Does TypeScript Structural Typing Work?

TypeScript utilizes a structural type system—often referred to as compile-time duck typing—where type compatibility and equivalence are determined solely by an object's actual shape and properties rather than its explicit name or declaration site. In contrast to nominal type systems found in languages like Java, C#, or C++, where two entities must share an explicit inheritance chain or declaration to be considered interchangeable, TypeScript considers any two types compatible if one possesses all the public members required by the other. This architectural choice aligns directly with the dynamic nature of JavaScript, allowing idiomatic runtime patterns while maintaining robust compile-time safety.

Understanding Structural Typing in TypeScript

In a structural type system, the compiler evaluates types by inspecting their internal structure. When values are assigned to a variable, passed as function arguments, or returned from functions, TypeScript verifies that the incoming value satisfies the target type's structural contract.

Consider this basic example:

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

function printCoordinates(point: Point2D) {
  console.log(`x: \({point.x}, y:\){point.y}`);
}

const vector = { x: 10, y: 20, z: 30 };
printCoordinates(vector); // Valid

Even though vector was not explicitly declared as an implementer of Point2D, and despite having an extra property z, TypeScript permits the call because vector satisfies the structural requirement of having numeric x and y properties.

Comparing Structural and Nominal Typing

The core distinction between structural and nominal typing lies in what establishes identity and compatibility:

Characteristic Structural Typing (TypeScript) Nominal Typing (Java, C#, Rust)
Compatibility basis Shape, members, and internal signatures Explicit name, namespace, and hierarchy
Type equivalence Identical structures are identical types Identical structures are distinct unless aliased or inherited
Declaration style Flexible, implicit matching Strict, explicit declaration (class B implements A)
Runtime overhead None (types are erased during compilation) Often preserved for runtime reflection and dispatch
Primary design goal Modeling dynamic JavaScript runtime patterns Strict domain modeling and type segregation

In a nominal language like Java, two classes with identical fields cannot be substituted for one another unless they share an explicit interface or parent class:

// Java (Nominal)
class User { public String id; }
class Order { public String id; }

User user = new Order(); // Compile-time error: incompatible types

In TypeScript, identical structures are mutually assignable:

// TypeScript (Structural)
class User { id!: string; }
class Order { id!: string; }

let user: User = new Order(); // Fully valid

Key Mechanics of TypeScript's Structural System

TypeScript applies structural checks across several language constructs, each governed by specific rules:

  • Object Width and Depth: Type compatibility checks verify that every required property in the target type exists in the source type with a compatible type. Excess properties on non-literal values are tolerated, accommodating typical JavaScript data extension patterns.
  • Excess Property Checks on Literals: While structural typing permits extra properties in intermediate variables, TypeScript enforces strict excess property checks on direct object literals. Writing printCoordinates({ x: 10, y: 20, z: 30 }) directly causes a compiler error to prevent typos and unused properties.
  • Function Parameter Bivariance and Contravariance: TypeScript checks function compatibility by evaluating parameter lists and return types. A function with fewer parameters can be assigned to a location expecting more parameters, which is essential for handling callback signatures in JavaScript libraries (such as Array.prototype.forEach).
  • Classes as Value-and-Type Pairs: A TypeScript class creates both a constructor function and an instance type. The structural comparison only evaluates the instance side (public properties and methods). Constructors and static members do not influence instance compatibility. However, private and protected members introduce nominal behavior: they must originate from the exact same class declaration to be considered compatible.

Simulating Nominal Typing in TypeScript

When strict domain separation is necessary—such as preventing an unvalidated identifier from being passed where a validated entity ID is expected—developers can simulate nominal typing using branded or tagged types:

type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };

function makeUserId(raw: string): UserId {
  return raw as UserId;
}

function deleteUser(id: UserId) {
  // Logic to delete user
}

const rawId = "user_12345";
const orderId = "order_67890" as OrderId;

deleteUser(rawId); // Error: string is not assignable to UserId
deleteUser(orderId); // Error: OrderId is not assignable to UserId
deleteUser(makeUserId(rawId)); // Valid

By intersecting a primitive type with a unique dummy property, the structural shape becomes impossible to satisfy accidentally, combining structural flexibility with nominal safety.