1.4 Structural vs Nominal Typing: What TypeScript Chose and Why
Right, let’s get this straight. You’ve probably heard TypeScript described as a “statically typed superset of JavaScript.” That’s true, but it’s also a bit of a corporate mission statement. The real magic, and the source of most of its “Wait, that compiles?!” moments, is its typing philosophy: structural typing.
Most languages you’re used to, like Java or C#, use nominal typing. In that world, a type’s identity is its name. If you have a class Dog and a class Cat, even if they have exactly the same properties (name, breed, age), you cannot assign a Dog to a Cat. Their names are different, end of story. It’s like trying to board a flight with a driver’s license—the government issued both, but the TSA agent really cares about the specific name on the card.
TypeScript, because it had to seamlessly work with JavaScript’s duck-typing, “if it quacks, it’s a duck” nature, chose the opposite path: structural typing. Here, a type’s identity is its shape. If two things have the same structure, they are the same type as far as the type checker is concerned. Their names are irrelevant.
interface Dog {
name: string;
breed: string;
}
interface Cat {
name: string;
breed: string;
}
const dog: Dog = { name: "Ein", breed: "Corgi" };
const cat: Cat = dog; // This is completely fine in TypeScript.
See that? We just assigned a Dog to a Cat variable. No errors. The type checker looked at both structures, saw that both have a name: string and a breed: string, and said, “Close enough for me!” The names Dog and Cat are just labels for our puny human brains; the type system only cares about the blueprint.
Why Structural Typing is a Genius Move for JS
This isn’t an academic choice. It’s a pragmatic one. JavaScript is riddled with objects that are created on the fly. Think of function parameters. In a nominally-typed world, you’d have to create a class for every single object shape you pass around.
// This is what nominal typing would force upon us. Nightmare fuel.
function printUser(user: UserClass) {
console.log(user.name);
}
// You'd have to `new UserClass({name: "Alice"})` every time.
// This is what structural typing allows. Beautiful.
interface User {
name: string;
}
function printUser(user: User) {
console.log(user.name);
}
// This works:
printUser({ name: "Alice" });
// And so does this, because it structurally matches:
const myObj = { name: "Bob", age: 31 };
printUser(myObj); // age is ignored, structure is satisfied.
It perfectly mirrors how JavaScript works. You don’t need to fuss with classes; you just need an object that has the right stuff. This makes TypeScript incredibly expressive for defining contracts between parts of your code without forcing a specific hierarchy.
The Dark Side: When “Close Enough” Bites You
Of course, this power comes with responsibility, and sometimes it leads to absolutely hilarious (or terrifying) situations.
The classic pitfall is when two semantically different things have the same structure. TypeScript will happily let you mix them up because it’s not a mind reader.
interface Point2D {
x: number;
y: number;
}
interface UserCoordinates {
x: number;
y: number;
}
const point: Point2D = { x: 5, y: 10 };
const coords: UserCoordinates = point; // Allowed, but are they the same thing?
// A more dangerous example
function calculateDistance(point: Point2D): number {
return Math.sqrt(point.x ** 2 + point.y ** 2);
}
const userInput = { x: 3, y: 4 }; // This could be from a form, meant for UserCoordinates
const distance = calculateDistance(userInput); // Compiles, works, but is it logical?
In this case, UserCoordinates might represent pixels on a screen, while Point2D represents a geometric point. Assigning one to the other is a logical error, but TypeScript can’t save you. The structure is identical.
How to Fight Back: Branding and as
So how do you tell the type system that two structurally identical things are not the same? You cheat. A common pattern is branding, where you add a unique, optional property to “nominalize” your type.
interface Point2D {
x: number;
y: number;
__brand: "Point2D"; // A unique literal type "brand"
}
interface UserCoordinates {
x: number;
y: number;
__brand: "UserCoordinates"; // A different unique literal type
}
const point: Point2D = { x: 5, y: 10, __brand: "Point2D" } as Point2D;
const coords: UserCoordinates = point; // NOW it's a Type Error!
// Type '"Point2D"' is not assignable to type '"UserCoordinates"'.
You have to use a type assertion (as Point2D) to create the branded object because you’re adding a property that isn’t in the initial literal. It’s a bit of a hack, but it’s an effective one for critical type distinctions. Use this power wisely, for when the semantic difference is truly important.
The bottom line? Structural typing is the reason TypeScript feels so fluid and JavaScript-y, not like a rigid, bureaucratic type system. It embraces the language it builds upon. But it also means the burden of defining correct semantic meaning—not just structural shape—falls on you, the developer. The type checker is brilliant, but it’s not a philosopher.