Don’t go crazy with the structural typing
I have a small axe I like to grind about the use and misuse of structural type systems. From the jump I must declare that, not being a PL researcher, I’m wading into waters above my head, so I ask forgiveness for inaccuracies. I’m bound to commit a few.
But, broadly, let’s talk about the two classes of static type systems: structural and nominal.
In nominal typing, the names of types are significant in determining an object’s membership in a type.1 Types are explicitly declared and relatively inflexible. Your type declares itself to implement a given interface, or subclass a given type, and the compiler checks that your declaration matches the requirements of that type then and there. (Think “Java”.)
In structural typing, it doesn’t matter what you name the type. What matters is that the properties of an object match the properties, or structure, of the type, at each use. (Think “TypeScript”.)
It’s my contention that structural typing lends itself to abuses that nominal typing does not, and as such, nominal typing is actually more conducive to making the code understandable. Now, you can’t always choose your type system, but you can choose how you use it.
What are types for?
At first blush, why wouldn’t structural typing have all the advantages? It seems to occupy a sweet spot of expressiveness and correctness guarantees. Nominal typing, in contrast, feels stodgy and limiting, a kingdom of nouns.
Types are for two things: correctness and understanding. Leaving aside correctness, let’s consider a similar case as it relates to understanding: I think it’s better for understanding’s sake to explicitly annotate types than to let the compiler infer them. It serves as a mental guardrail: the reader knows, without leaning on IDE support, what types a given function accepts and returns. It serves as a contract, too: if the code changes, the return type is not implicitly changed without it being brought to your attention.
Nominal typing means cognitive load stops (all else being equal) at the name of the type.
There is nothing that difficult about structural typing per se: it’s the higher-level type constructions that such a system allows you to express. Structural typing allows you to be clever. And in programming, we know how cleverness ends.
Here’s where we get to pick on TypeScript!2 TypeScript offers you an algebra of type constructors for making new types out of other types. You can compose these in arbitrarily complex ways, to the point where it’s very difficult to understand the properties of the resulting type. Combine that with various stateful, magical JavaScript frameworks—lookin’ at you, Redux and friends—and it’s very hard to reason about the types you’re dealing with, to the point where if you haven’t memorized the types from similar examples elsewhere in the code, there’s very little chance you could come up with the correct one.
Structural typing can produce effects that are magical.
On not being a thing
There’s an expression I love in the English language: the state of being a thing, or, conversely, not a thing. I think this captures perfectly the kinds of constructions you want to avoid in structural typing land.
When I think of a structural type that is not a thing it means you have drawn a boundary, with type constructors, around a shape that doesn’t correspond to any intelligible concept we can really hold in our heads. It doesn’t correspond to any meaningful object in the domain, the architecture, or the business. Instead, you have bolted types together, or lumped them together, in ways that are arbitrary enough, in order to get a particular peg to fit into a hole. When I look at types like these, I think: this is not a thing.
The great virtue of nominal typing is that it pushes you in the direction of making your types a thing. You must name each type, and if you can’t find a good name, chances are your type is not a thing. If you find yourself writing a “Utilities” or a “Helper” or a “Manager” or anything similarly vague, that’s a signal you pay attention to, and you reflect: have you not defined your architecture very well? Are you lumping together functionality that doesn’t really cohere?