Type systems are a fundamental part of programming language design, and often provide the very basis of a language. Deciding how types relate is essential to a type system's design.
Type relationships are important in understanding type compatibility . Two types are compatible in a particular context, if they can be interchanged without causing a type error (noting this does not mean ‘without causing a difference in behaviour’ - two types can have very different behaviours and still be compatible).
Nominal type systems determine the relationships between types using explicit declarations. Subtyping relationships are determined by explicit definitions - most commonly by implementing an interface or extending a class.
<?php//NOMINAL interface Animal { function eat(): void;} // Dog is a subtype of Animal// as it is explicitly declaredinterface Dog extends Animal { function bark(): void;}
Structural type systems determine the relationships between types using their structures. Subtyping relationships are determined by the presence of members (fields and methods) - for type A to be a subtype of type B, A must have all of the members of B (with compatible types).
// STRUCTURAL interface Animal { eat(): void;} // Dog is a subtype of Animal// as all of Animal's members are presentinterface Dog { eat(): void; bark(): void;}
This may feel confusing - a subtype in a nominally typed system also has all of the members of its supertype? The distinction here is that in a nominally typed system, type A having all the members of type B does not make it a subtype - it just allows it to be. In a nominally typed system it is possible to have a type A which has all of the members of B, without it being a subtype. In a structurally typed system, it is not.
<?php interface Animal { function eat(): void;} // Subtype in structurally typed system// Not subtype in nominally typed systeminterface Dog { function bark(): void; function eat(): void;}
At the simplest level, the trade-off between nominal and structural type systems is yet another case of robustness versus flexibility. By requiring subtyping definitions to be explicit, nominally typed systems force programmers to think about which types should be allowed for a given value. This makes it much harder to have ‘accidental subtypes’. Additionally, providing names to subtyping relationships can help convey programmer intent. On the flip side, structural type systems can often result in less type boilerplate. Take the following nominally typed code for example:
<?php// NOMINAL interface Flyable { function fly(): void;} interface Swimmable { function swim(): void;} interface Walkable { function walk(): void;} class Duck implements Flyable, Swimmable, Walkable { function fly(): void {} function swim(): void {} function walk(): void {}} class Penguin implements Swimmable, Walkable { function swim(): void {} function walk(): void {}} // Duck and Penguin are both valid Swimmables// As they explicitly implement the interfacefunction swimmersOnly(Swimmable $swimmable): void{ $swimmable->swim();}
Now comparing with the following structural equivalent, there is much less boilerplate code involved:
// STRUCTURAL interface Flyable { fly(): void;} interface Swimmable { swim(): void;} interface Walkable { walk(): void;} class Duck { fly(): void {} swim(): void {} walk(): void {}} class Penguin { swim(): void {} walk(): void {}} // Duck and Penguin are both valid Swimmables// As they contain all the members of the interfacefunction swimmersOnly(swimmable: Swimmable): void{ swimmable.swim();}
(Truthfully I’m finding it quite difficult to cover this advantage at such a small scale. Imagine a codebase with many more classes and interfaces, and this becomes much more apparent.)
Another advantage of structural typing, is its interaction with third party code. Following on from the above example - imagine a third party library introduces a class Fish:
class Fish { swim(): void {}}
With it being third party code, we lack the ability to add our custom Swimmable interface here. For nominally typed systems, this causes a problem: Fish cannot be passed to our swimmersOnly function. In a structurally typed system - Fish works just like a first party class.
In reality, the disadvantages (and differences in general) can be narrowed by other features of the type system. For example getting around the issue of 'accidental subtyping' in a structural type system, types may declare unique properties (see TypeScript demo) . It’s worth noting that these workarounds usually end up resulting in more boilerplate than their nominal counterparts.
Most type systems are not entirely structural or entirely nominal. In the above examples, I have used PHP to illustrate nominal typing, and TypeScript to illustrate structural typing (albeit with some strange coding styles, to make them appear more similar). TypeScript is not entirely structurally typed - for example any type with a private or protected member can no longer have a structural subtype.
class Animal { protected eat(): void {}} class Dog { protected eat(): void {} public bark(): void {}} function animalsOnly(animal: Animal): void {} // Invalid: Types have separate declarations of a private property 'eat'animalsOnly(new Dog);
Instead, subtyping involving these methods must be nominal:
class Animal { protected eat(): void {}} class Dog extends Animal { protected eat(): void {} public bark(): void {}} function animalsOnly(animal: Animal): void {} // Valid: Dog is a nominal subtype of AnimalanimalsOnly(new Dog);
Blending aspects of structural and nominal typing often brings the best of both worlds to a type system: clarity and flexibility. Real-world scenarios often necessitate a middle ground, allowing leveraging the explicit relationships and intentionality of nominal typing with the adaptability and code reuse offered by structural typing.
Thanks for reading, I hope it was of value. As always, any feedback is greatly appreciated!