Structural Typing in JavaScript: Lessons for Robust Design
Structural typing isn’t new. If you’ve used TypeScript or played with languages like Go or Elm, you’ve probably already run into it, even if you didn’t think much about it. What is worth thinking about, especially for seasoned JavaScript developers, is how structural typing can nudge us toward more robust, intention-revealing designs, even in vanilla JS environments.
This post is less about definitions and more about implications. So let’s skip the textbook intro and get to why this matters in the trenches.
Structural Typing in Practice
In JavaScript, you’re not working in a structurally typed language per se. But the dynamic nature of JS means that behavior often ends up being “structural” by default. If an object has the right shape (i.e., the right properties or methods) then you treat it as the thing you need.
function logUser(user) {
console.log(user.name);
}
logUser({ name: "Alice", age: 30 }); // Works fine
This isn’t surprising to anyone who writes JavaScript daily. But here’s the thing: most JS devs treat this as a side-effect of a loose type system. When you’re using TypeScript (or some type layer on top of JS), you start seeing it more explicitly.
type Person = { name: string };
function greet(p: Person) {
console.log(`Hello, ${p.name}`);
}
const employee = { name: "Bob", role: "Engineer" };
greet(employee); // Totally fine in TypeScript
This is structural typing. TypeScript doesn’t care about employee
being a Person
by name, only that it has the right structure.
Why It Matters: Interface by Shape, Not by Name
One consequence of this: you can define contracts in your code without obsessing over rigid class hierarchies or nominal interfaces. It aligns closely with how JavaScript is already written: small, focused objects that conform to certain expectations.
This can encourage a more compositional style. Instead of building deep inheritance chains or massive abstract interfaces, you think about what behaviors your functions rely on.
Take an example from UI component APIs. You might write something like this:
type Clickable = {
onClick: () => void;
};
function attachClickHandler(component: Clickable) {
component.onClick();
}
Now any object with an onClick
method qualifies. That includes React components, DOM elements with manually attached handlers, or even just plain objects in tests. You don’t care what they are, only what they can do.
Structural Typing vs. “Duck Typing”
It’s worth clarifying: structural typing is not the same as duck typing. Duck typing is more about runtime behavior, “if it quacks, it’s a duck.” Structural typing is more formalized, static, and reasoned about at compile time (or at least at lint time with tools like TypeScript or Flow).
The confusion often comes because both styles allow for flexible interop. But where duck typing fails silently, structural typing gives you guardrails.
type Flyable = { fly: () => void };
function launch(obj: Flyable) {
obj.fly();
}
launch({ fly: () => console.log("Flying!") }); // Okay
launch({ run: () => console.log("Running") }); // Type error
In this case, structural typing helps document and enforce your intent without needing a base class or explicit inheritance.
When Structure Bites Back
But structure isn’t a silver bullet. There are real tradeoffs, especially when different parts of the system evolve independently.
Say you’ve got a UserProfile
and AdminProfile
, both of which accidentally match the shape { name: string, email: string }
. You may intend for them to be handled differently, but structural typing won’t stop you from mixing them up if they look the same.
type UserProfile = { name: string; email: string };
type AdminProfile = { name: string; email: string };
function sendUserNotification(profile: UserProfile) {
// ...
}
const admin: AdminProfile = getAdmin();
sendUserNotification(admin); // No compile error, maybe not what you intended
You can reach for branding or nominal typing tricks here, but it becomes a matter of discipline. Structural systems don’t guard intent, they just check shape. That’s powerful, but it cuts both ways.
Lessons for Design
So what do we take from this?
Design to capabilities, not hierarchies. If you’re writing functions that operate on “things with a
save()
method,” define your types that way. Avoid assuming consumers will subclass your base class.Favor small, intersecting types. Combining narrow type definitions is often cleaner than building large, monolithic ones. TypeScript’s intersection types (
&
) make this especially ergonomic.Document intent with types. Even if two types share a shape, they may represent different concepts. Use naming and structure to encode meaning, not just form.
Watch for accidental compatibility. Structural typing makes some errors look valid. Be explicit where it matters, particularly around domain boundaries or security-sensitive code.
Lean into tests, not just types. Structural typing is a helpful abstraction tool, but your tests still need to assert behavior, not just shape.
Final Thoughts
Structural typing reflects something JavaScript developers have long done: rely on shape over identity. But thinking more deeply about it, especially with TypeScript in the mix, can help you design cleaner, more intention-driven APIs.
It won’t save you from every mistake, but it does shift the burden from “how is this built?” to “what does this support?” And that’s a shift worth embracing.