Structural vs. Nominal Type Systems
Iāve been interested in type systems lately. You hear a lot about static vs. dynamic type systems, and how static is clearly better. I would agree with that.
But there is another way to divide them up āĀ structural vs. nominal type systems.
Structural Typing
In a structural type system, two types are compatible if they have the same structure. TypeScript is the quintessential example of a structural type system.
But structural typing can have problems āĀ sometimes slippery boundaries are not ideal.
You can mitigate this by giving each type a ābrandā.
And now weāve stumbled into nominal typing.
Nominal Typing
In a nominal type system, types are only compatible if they have the same name (or ātagā, or ābrandā).
They are stricter than structural type systems āĀ in fact, nominal typing is a subset of structural typing. Note that our above example was doing nominal typing in one of the most structural type systems out there, TypeScript.
Hereās our original example in Kotlin,Ā a great example of a nominal type system.
Letās review some definitions.
Subtyping
Subtyping is about substitutability. Type S
is a subtype of type T
if S
can be used in a place where T
is expected.
In our original TypeScript example, State
is a subtype of Country
, and Country
is a subtype of State
.
When subtyping goes both ways, we say the types are equivalent. Often times the relationship only goes one way though.
We can use Auth0User
in place of User
, making Auth0User
a subtype of User
.
We cannot use User
in place of Auth0User
, meaning User
is not a subtype of Auth0User
.
Nominal Subtypes
In a nominal type system, a type is only a subtype of another if it is declared to be so in its definition. Structural subtyping is intrinsic, while nominal subtyping is declarative [3].
Iām going to repeat that for emphasis.
Structural subtyping is intrinsic, while nominal subtyping is declarative.
Letās look at a subtype example in our nominal type system of the day, Kotlin.
What about Go?
Go is another language Iām fond of, and itās not as black and white as TypeScript and Kotlin.
Letās take a look at the Country
example in Go.
Itās the same for passing arguments to a function.
So, nominal, right? Not quite.
If we donāt use a named type in our function definition, then it matches based on structure.
In Go,
- two types are either identical or different (no subtyping)
- a named type is always different from any other type
- otherwise, theyāre identical if their underlying structures are equivalent
Go also defines the notion of assignability (which sounds a lot like āsubstitutabilityā). One type is assignable to another if:
- theyāre the same named type
- they have the same underlying structure, and at least one of them is not a named type
- one is an interface type that the other implements
So Go is a mixed bag. It is structurally typed in general, but then it has this thing called named types, which is the definition of nominal typing. Not to mention using them is an extremely common way to write Go.
Final Questions
How does duck-typing relate to structural typing?
Citing this answer, duck typing is a runtime phenomenon emerging from the semantics of dynamically typed languages.
TypeScript is structurally typed. JavaScript is duck-typed.
Iām not a huge fan of duck typing, because Iām not a huge fan of dynamic type systems. Structural typing is like a static version of duck typing. And that makes it better.
Do nominal types maintain type information at runtime?
Consider our TypeScript branding example.
That _unit
brand is going nowhere at runtime, because we have to do this.
What about in Kotlin? In short, yes.
Kotlin has the notion of runtime type information, which is avaliable for some of its types, namely classes, interfaces, objects, and functions. Check out this section of the spec.
The key exception is generics. Those types are erased.
In Summary
- Nominal typing is a subset of structural typing. In structural typing, type compatibility is decided on the structure of types. In nominal typing, itās decided by name. You can achieve this in TS with branding.
- TypeScript is structurally typed.
- Kotlin is nominally typed.
- Type relationships are sometimes managed through subtyping. One type is a subtype of another if it can be substituted in its place. Go does not do subtyping; the closest thing it has is assignability.
Resources
- Effective TypeScript
- Wikipedia
- Integrating Nominal and Structural Subtyping
- Nominative and Structural Typing
- Comparison of Programming Languages by Type Systems
- Kotlin Language Specification, Type System
- Programmer dictionary: Class vs Type vs Object
- Why doesnāt Go have variance in its type system?
Wow! You read the whole thing. People who make it this far sometimes
want to receive emails when I post something new.
I also have an RSS feed.