---
title: "05 — Structural Typing: Shapes, Not Names"
uid: structural-typing
tags: ["typescript", "type-compatibility", "structural-typing", "roadmap:typescript"]
excerpt: "TypeScript is structurally typed — two types are compatible if they have the same shape, regardless of what they're called. The structure decides, not the label."
date: 2026-08-13T03:27:29+0000
source: https://www.aveshina.my.id/en/blog/structural-typing
---

"TypeScript checks whether the names match" was my compatibility model, and it predicted the wrong errors. Writing it down corrected the model entirely: **TypeScript is structurally typed. Two types are considered compatible if they have the same shape, even if they were declared with completely different names.** [1] What matters is the structure — which properties exist and what their types are — not the label on the type.

This is the single biggest philosophical difference between TypeScript and languages like Java or C#, which are *nominally* typed. In a nominally typed language, a Point class and a Coordinate class with identical fields are incompatible — the names differ. In TypeScript, they're interchangeable [1]:

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

const p: Point = { x: 10, y: 20 };
const c: Coordinate = p; // perfectly valid
```

Despite the different names, p assigns to c without complaint. The compiler looked at the structure — both have x: number and y: number — and concluded they're compatible. The names are decorative.

```figure
<svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Structural vs nominal typing. Left side labelled Point and right side labelled Coordinate each contain x: number and y: number. A green checkmark between them shows TypeScript accepts the assignment based on matching shape, ignoring the names.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Point -->
    <rect x="40" y="50" width="240" height="140" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="160" y="76" font-size="14" font-weight="700" fill="#1e1b4b" text-anchor="middle">interface Point</text>
    <rect x="60" y="92" width="200" height="26" rx="4" fill="#c7d2fe" stroke="#6366f1" stroke-width="1"/>
    <text x="160" y="109" font-size="11" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">x: number</text>
    <rect x="60" y="124" width="200" height="26" rx="4" fill="#c7d2fe" stroke="#6366f1" stroke-width="1"/>
    <text x="160" y="141" font-size="11" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">y: number</text>
    <text x="160" y="178" font-size="10" font-style="italic" fill="#475569" text-anchor="middle">named "Point"</text>

    <!-- check -->
    <circle cx="360" cy="120" r="22" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <path d="M350,120 L358,128 L372,112" fill="none" stroke="#16a34a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>

    <!-- Coordinate -->
    <rect x="440" y="50" width="240" height="140" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="560" y="76" font-size="14" font-weight="700" fill="#500724" text-anchor="middle">interface Coordinate</text>
    <rect x="460" y="92" width="200" height="26" rx="4" fill="#fbcfe8" stroke="#db2777" stroke-width="1"/>
    <text x="560" y="109" font-size="11" font-family="ui-monospace, monospace" fill="#500724" text-anchor="middle">x: number</text>
    <rect x="460" y="124" width="200" height="26" rx="4" fill="#fbcfe8" stroke="#db2777" stroke-width="1"/>
    <text x="560" y="141" font-size="11" font-family="ui-monospace, monospace" fill="#500724" text-anchor="middle">y: number</text>
    <text x="560" y="178" font-size="10" font-style="italic" fill="#475569" text-anchor="middle">named "Coordinate"</text>

    <!-- note -->
    <text x="360" y="222" font-size="11" fill="#64748b" text-anchor="middle" font-style="italic">same shape → compatible, names ignored</text>
  </g>
</svg>
```

## Why structural typing fits JavaScript

The choice isn't accidental. JavaScript is full of anonymous object literals — { x: 10, y: 20 } — created on the fly and passed around. A nominally typed system would demand that every such object declare an explicit class, which fights how JavaScript is written. Structural typing meets the language where it is: an object is shaped like a Point, so it *is* a Point, no ceremony required. This is also why adopting TypeScript is gradual — a plain object literal from JavaScript code already satisfies any matching interface.

## The rule, more precisely

The compiler walks the properties. For type A to be assignable to type B, every property required by B must be present in A with a compatible type. Note the directionality: A can have *extra* properties that B doesn't mention, and that's fine — B only asks for its own properties to be satisfied. (There's a subtlety with *fresh* object literals, where excess properties trigger an error, but for already-typed variables the "extra is fine" rule holds.)

This is why a function that accepts { x: number; y: number } will happily take a User object that happens to also have x and y among its fields. The function only cares about the shape it uses.

## The footgun: accidental compatibility

The flip side of "shape is all that matters" is that two types can be *accidentally* compatible. An object with more fields than required still satisfies a narrower type, which is usually fine — but it means a typo or an unexpected field won't always be caught at the assignment site. The compiler trusts the shape, so when shapes coincidentally overlap, so does compatibility. This is the trade-off for the flexibility, and it's why precise types (literal types, branded types, discriminated unions) become valuable as a codebase grows.

## How I use this

The practical upshot is that I design types around shapes, not hierarchies. I don't force unrelated types to share a base class or interface just so a function can accept them — if they have the right structure, the function takes them, and that's idiomatic. When I want to *prevent* accidental compatibility (say, two ID types that are both string but shouldn't be mixed), I reach for branding or nominal patterns, because plain structure won't distinguish them. The mental habit is to ask "what shape does this function need?" rather than "what type name should this be?" — and that shift is, for me, the whole point of TypeScript's design.

## References

[1] Microsoft, "Type Compatibility," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/type-compatibility.html](https://www.typescriptlang.org/docs/handbook/type-compatibility.html)

[2] Microsoft, "Advanced Topics in Type Compatibility," TypeScript Handbook, 2024. [Online]. Available: [https://www.typescriptlang.org/docs/handbook/type-compatibility.html#advanced-topics](https://www.typescriptlang.org/docs/handbook/type-compatibility.html#advanced-topics)

```quiz
Q: TypeScript decides whether type A is assignable to type B by…
- comparing the type names
- comparing the structure — the properties and their types
correct: 1
explain: TypeScript is structurally typed. If A has every property B requires with compatible types, they're compatible, regardless of names.

Q: A `Point` interface and a `Coordinate` interface both define `{ x: number; y: number }`. Can a `Point` be assigned to a `Coordinate` variable?
- Yes — they have the same shape
- No — the names differ
correct: 0
explain: Names are decorative under structural typing. Matching shape means compatibility, so the assignment is valid.

Q: An object has `{ x: 10, y: 20, z: 30 }` and is assigned to a variable typed `{ x: number; y: number }`. What happens (for an already-typed object)?
- Error — extra properties are forbidden
- Valid — extra properties are allowed; the target only requires x and y
correct: 1
explain: For already-typed variables, having extra properties beyond what the target requires is fine. The target only asks that its own properties be satisfied.

Q: Why does structural typing suit JavaScript?
- because JavaScript is nominally typed at runtime
- because JavaScript is full of anonymous object literals, and shape-matching meets the language where it is
correct: 1
explain: JavaScript code constantly creates object literals on the fly. Structural typing lets those literals satisfy matching interfaces without forcing explicit class declarations.

Q: You have two types that are both `string` (UserId and OrderId) that should NOT be interchangeable. Structural typing means they ARE compatible. To prevent mixing, you reach for…
- nothing — TypeScript will catch it
- branding or a nominal pattern, since structure alone won't distinguish two strings
correct: 1
explain: Plain structural typing treats identical shapes as compatible. To distinguish same-shaped types, you need branding or a nominal technique layered on top.
```
