← Back to blog

Nominal vs. Structural Typing Is the Medieval Realism-Nominalism Debate, 700 Years Early

One side of a seven-hundred-year argument has already won inside your codebase. You just weren't told which.

Published June 2026 · 13 min read

A function asks for a velocity. You hand it a position. Both are { x: number; y: number }, so TypeScript shrugs, lets it through, and your simulation runs, printing numbers that are wrong in a way no stack trace will ever point at. The types matched. The meanings didn't.

That is the most ordinary bug in a structurally typed language, and the argument about whether it's even a bug is seven hundred years old. One medieval philosopher would have told you it was inevitable: any system that decides what a thing is by looking only at its shape will eventually call two unlike things the same. A different medieval philosopher would have told you it isn't a bug at all, shape is the only thing there ever was to check, so a velocity shaped like a position simply is a position, and you were naive to expect otherwise. They thought they were arguing about God, grammar, and the furniture of reality. They were also, it turns out, arguing about your type system. One of them has already won inside your codebase. You just weren't told which.

Here is the claim, and it isn't a metaphor I'm stretching for effect: when a language designer chooses nominal or structural typing, they are taking a side in the medieval problem of universals, the longest-running argument in Western metaphysics, and the vocabulary we inherited points exactly backwards about who stands where. This connection floats around developer circles as a one-liner; what almost no one does is follow it all the way down, to where the medieval objections to each side turn out to be the exact bug classes each typing discipline produces today, stack traces included.

The question a Greek refused to answer

Around 270 CE, the philosopher Porphyry sat down to write a beginner's introduction to Aristotle's logic, the Isagoge, literally “the introduction.” In the opening pages he raised three small questions about general words like human and animal. Do the kinds they name actually exist out in the world, or only in the mind? If they exist, are they physical or not? And are they somehow inside the individual things, or standing apart from them? Then he did something consequential: he declined to answer. The questions were, he said, too deep for an introductory text.

Boethius translated the Isagoge into Latin around 510 CE, and for the next thousand years it was the book Europe learned logic from. The main influence of the Isagoge, historians like to note, lies not in what it says but in what it refuses to say. Commentators found Porphyry's silence intolerable. They fell, one century after another, straight into the gap he'd left, and the problem of universals was born from a footnote the author wouldn't write.

Three answers crystallized. Realism: the universal is a real entity. There genuinely is a thing, humanity, that you and I both share, and it's the sharing of that real common nature that makes us both human. Conceptualism: the universal is a concept in the mind, formed when we notice real similarities among particular things, real enough as a mental sign, but not a free-floating entity in the world. Nominalism: there is no universal at all. Only particular things exist; “human” is just a name we apply to a pile of similar particulars, and the name marks no shared essence because there is no essence to mark.

Now look at your type system, because it has quietly answered Porphyry.

The words betray you

Nominal typing is the rule in Java, C#, C++, Kotlin, Swift, and Rust: two values are the same type only if they were declared to share that named type. Same shape isn't enough. A struct Meters(f64) and a struct Feet(f64) are different types forever, even though both are just a float in a trench coat, because you said so. The name is law.

Structural typing is the rule in TypeScript, in Go's interfaces, in OCaml's object types: two types are compatible if they have the same structure. If a value has the right shape, it is the type. The name is a convenience; the shape is the truth. (Dynamic languages push this further into “duck typing,” which is structural typing evaluated at the last possible second.)

Here's the trap, and it's the best part. Both “nominal typing” and “nominalism” come from the same Latin word, nomen, “name.” You'd reasonably guess they're allies. They are opposites.

In nominal typing, the name is authoritative and constitutive: being declared a Foo is what makes you a Foo, and that declaration marks membership in a real, distinct kind. That is the realist move exactly: the name names a real essence. In nominalism, the name is merely a name, a label thrown over particulars with no real kind underneath. That is the anti-realist move, and it's the one structural typing actually operationalizes, because a structural type name like Vector2D was never more than shorthand for “anything with this shape.”

So the dictionary lies to you. Nominal typing is realism. Nominalism is structural typing. Both camps obsess over the name; they just disagree about whether the name marks a real kind or papers over a heap of look-alikes. Spring that trap on yourself once and the whole mapping clicks into place.

You can watch a single person take both sides. Anders Hejlsberg is the lead architect of C#, which is strictly nominal, and the lead architect of TypeScript, which is thoroughly structural. He didn't change his mind between the two; he changed the problem. C# sits on top of a runtime full of declared, named kinds, so it enforces declared membership. TypeScript sits on top of JavaScript, a swarm of anonymous object literals with no declared kinds at all, so it checks the only thing that's actually there, the shape. The same designer answered Porphyry's question twice, in opposite ways, because the right answer depends on what your particulars are made of, a luxury the medievals never had, convinced as they were that there was one true answer and your soul might hang on it.

Getting Ockham right (and meeting the man condemned for getting it wrong)

It's tempting to crown William of Ockham (c. 1287–1347) the patron saint of structural typing and stop there, “Ockham, nominalism, it's just a name” is the version everyone half-remembers. But the careless version gets him wrong, and the correction sharpens the analogy.

Ockham is more precisely a conceptualist-leaning nominalist. For him the universal is a mental concept, a sign caused in the mind by perceiving genuine similarities among real particulars. It has no existence outside the mind, but it is emphatically not empty noise. It's grounded in the way things actually resemble each other. That is structural typing to the letter: the type is the common structure abstracted from the particulars that happen to have it. Not a real kind floating in the world (that's realism / nominal typing), and not arbitrary either, anchored in real shared shape.

The man who did hold the “it's just noise” position came earlier and paid for it. Roscelin of Compiègne (c. 1050–c. 1121) taught that universals are nothing but flatus vocis, the breath of the voice, nothing more. Pure duck typing: the name doesn't even matter, only the thing in front of you right now. Roscelin then made the mistake of applying this to the Trinity. If “God” is just a name with no real shared nature behind it, he reasoned, then the three Persons must be three separate things. The Church heard tritheism. He was hauled before the Council of Soissons in 1092 and made to recant on pain of excommunication, and, by some accounts, of being stoned by the faithful. The stakes on “is the universal real?” were once your standing with God and the safety of your skin. Today they're a duck-typed collision in a graphics routine. Progress, of a kind.

The precision lets you lay out a spectrum instead of a binary:

Position on universalsTyping analogue
Extreme realism (one shared essence, wholly in each thing)Strong nominal typing, only declaration counts
Moderate realism (real kind, but in things, abstracted by intellect)Nominal typing with interface conformance (implements)
Conceptualism (Ockham: concept grounded in real similarity)Structural typing, shape abstracted from the particulars
Extreme nominalism (Roscelin: mere sound)Duck typing, only the behavior at the call site

And then there's the detail that ought to make every working engineer sit up. The single most-quoted principle in software design is Ockham's Razor, and the founder of nominalism handed it to us as an argument for structural typing.

The bugs were predicted in the twelfth century

This is where a clever analogy becomes something with teeth. The centuries-old objections to each side of the universals debate are not vaguely similar to the failure modes of each typing discipline. They are the same objections, now reproducible on a build server.

The standing objection to realism is that it multiplies entities. If every meaningful predicate names a real universal, your ontology bloats with a separate real essence for redness, humanity, chairness, and on forever. The medieval reply to that bloat is Ockham's Razor itself: Numquam ponenda est pluralitas sine necessitate, “plurality must never be posited without necessity.” (The catchier “entities must not be multiplied beyond necessity” that everyone attributes to Ockham appears nowhere in his surviving work; an Irish Franciscan named John Punch coined that phrasing in 1639. Even the Razor is a structural type imposed on a particular.)

In code, realism's multiplied entities are nominal typing's rigidity tax. The newtype wrappers. The adapter and conversion boilerplate. The function that won't accept a structurally identical value because it wasn't declared the right kind, so you write the fortieth tiny From/Into impl to launder a float into a float. Every engineer who has cursed at nominal ceremony has, without knowing it, been re-running Ockham's complaint against the realists: you are positing kinds without necessity, and I am holding the razor.

The standing objection to nominalism is that it loses the ground for why a classification is correct. If “human” is just a name for similar-looking particulars, what stops you from filing genuinely unlike things under one label? Nothing in the theory does. And that is precisely structural typing's accidental-collision bug, the velocity you passed where a position was wanted, back at the top of this essay. Point2D { x, y } and Vector2D { x, y } silently unify. Meters and Feet, left as bare numbers, add together and typecheck and lie. The medieval worry that nominalism can't prevent accidental sameness is the duck-type collision, seven hundred years early, and it ships to production.

Here's the empirical verdict the medievals never got to see, because they couldn't run the experiment: both sides keep reinventing the other. TypeScript engineers, tired of accidental collisions, bolt on brand types, type Vector2D = { x: number; y: number } & { readonly __brand: 'Vector2D' }, which does nothing at runtime and exists purely to forge a declared, distinct kind on a structural substrate. That's nominalism's substrate reaching back for realism. Rust's newtype pattern is the same instinct in a nominal language that wanted units kept separate. Going the other way, Rust's traits and Go's interfaces bolt structural conformance onto otherwise nominal-ish worlds, so that anything implementing Write([]byte) satisfies io.Writer with no declaration of allegiance. No major language is purely one thing: Go is structural in its interfaces and nominal in its named types; Rust is nominal with structural escape hatches; TypeScript is structural with nominal cosplay on request. Every mature type system ends up needing both intuitions, which is the closest thing to a resolution the seven-hundred-year debate has ever received: it's unresolved because both sides are load-bearing.

The honest catch: a laboratory, not a metaphysics

I have to flag where this stops, or a philosopher will flag it for me, and they'll be right.

The medieval debate was a claim about mind-independent reality. Roscelin, Ockham, and the realists thought there was a fact of the matter about whether humanity is a real universal, and they were trying to discover it. Your type system is not like that. A language designer doesn't discover whether Vector2D is really distinct from Point2D; they stipulate it, by fiat, trading rigidity against accidental matches. There is no metaphysical truth the compiler is tracking. You “answer Ockham” the way you answer a configuration prompt, not the way you answer a question about reality.

So what transfers is the structure of the arguments and the geometry of the tradeoff, not the truth-claim. Say that plainly and the analogy gets stronger, not weaker, because it means your type system is something the medievals would have killed for: a laboratory where their tradeoffs actually run, with consequences you can count. They could only argue about whether accidental sameness was a real danger. You can grep your incident log for it.

What this is good for on Monday

The practical payoff is a decision rule, and it's sharper than “use whichever your language defaults to.”

Reach for the realist tool (nominal types, newtypes, brands) exactly where accidental sameness is dangerous. Units. Currency. Identifiers from different tables that happen to both be strings. Security tokens. A sanitized string versus a raw one. These are the cases where two things sharing a shape but not a meaning is a latent disaster, not a convenience. The canonical disaster is literal: in September 1999, NASA's Mars Climate Orbiter broke apart in the Martian atmosphere after a ten-month flight because one software module reported thruster impulse in pound-force-seconds and the module consuming it assumed newton-seconds. Two numbers, same shape, different meaning, silently unified. A Newtons newtype that refused to add to a PoundForce would have turned a quarter-billion-dollar loss into a compile error. That is realism earning its rigidity tax in one stroke.

Reach for the nominalist tool (structural types, interfaces, duck typing) where frictionless polymorphism pays and collisions are cheap. Plumbing. Glue code. “Anything with a .read() method.” io.Writer is structural for a reason: the universe of things you might want to write to is open-ended, and forcing each one to declare fealty would be exactly the multiplied entities the Razor warns against.

The mistake isn't picking a side. The mistake is not noticing there is a side, and letting your language's default answer Porphyry's question for you in cases where you'd have answered the other way. So when you reach for a brand type or a newtype, recognize the gesture: you're bolting a real kind onto a system that only believes in shapes, re-litigating 1092, with less at stake and better tooling. And when you delete a needless wrapper, you're handing Ockham his razor back.

The debate the medievals couldn't finish runs in your build pipeline now, both intuitions pulling against each other in every codebase that ever added a Branded<T>. They never got a compiler to keep the score. You do. The least you can do with seven hundred years of their argument is stop letting the default win by accident.


Sources: Porphyry, Isagoge (c. 270 CE), and Boethius's Latin translation and commentaries (c. 510 CE), the origin and transmission of the three questions on universals. Stanford Encyclopedia of Philosophy, “The Medieval Problem of Universals”; “William of Ockham” (Spade & Panaccio); and Paul Vincent Spade, Five Texts on the Mediaeval Problem of Universals (Hackett, 1994), realism/conceptualism/nominalism; Roscelin's flatus vocis and the Council of Soissons (1092); Ockham as conceptualist-leaning nominalist. On the Razor's wording: W. M. Thorburn, “The Myth of Occam's Razor” (1918), tracing “Entia non sunt multiplicanda praeter necessitatem” to John Punch (1639), distinct from Ockham's own “Numquam ponenda est pluralitas sine necessitate.” Language classifications (Java, C#, C++, Kotlin, Swift, Rust vs. TypeScript, Go, OCaml) per standard nominal/structural/duck-typing references. Anders Hejlsberg as lead architect of both C# and TypeScript (Microsoft; biographical and interview sources). NASA, Mars Climate Orbiter Mishap Investigation Board report (1999), the pound-force-seconds vs. newton-seconds unit mismatch. The transferable content is the structure of the arguments and the geometry of the rigidity-vs-collision tradeoff, not the medievals' metaphysical truth-claim (a type distinction is stipulated, not discovered); within that boundary the mapping is a near-identity, with each side's classical objection reproduced as the other discipline's bug class.

Trusting an agent by its shape is the accidental-collision bug at the identity layer.

The essay's whole danger is structural identity: two things that share a shape but not a meaning, silently unified, units, currency, sanitized strings, and the one it lists that should worry you most, security tokens. An autonomous agent is the same trap waiting to happen. Accept anything that looks like your trusted agent (same API, same self-reported name) and you have duck-typed your trust boundary, the Mars Climate Orbiter of the agent economy. Chain of Consciousness gives an agent a nominal, declared identity instead of a structural one: every action anchored to a tamper-evident provenance chain, so a trusted agent is the one that can prove its real history, not merely the one shaped like it.

See a verified action chain · Hosted Chain of Consciousness

pip install chain-of-consciousness  ·  npm install chain-of-consciousness