Type Systems: Structural vs. Nominal typing explained

Please do not link to this article on Reddit or Hacker News.

An important attribute of every type system is whether they are structural or nominal, they can even be mixed within a single type system. So it’s important to know the difference.

A type is something like a string, a boolean, an object, or a class. They have names and they have structures. Primitives like strings or booleans have a very simple structure and only go by one name.

More complex types like object or classes have more complex structures. They each get their own name even if they sometimes have the same structure overall.

A static type checker uses either the names or the structure of the types in order to compare them against other types. Checking against the name is nominal typing and checking against the structure is structural typing.

Nominal typing

Languages like C++, Java, and Swift have primarily nominal type systems.

class Foo {
method(input: string): number { ... }
}
class Bar {
method(input: string): number { ... }
}
let foo: Foo = new Bar(); // ERROR!!

Here you can see a pseudo-example of a nominal type system erroring out when you’re trying to put a Bar where a Foo is required because they have different names.

Structural typing

Languages like OCaml, Haskell, and Elm have primarily structural type systems.

class Foo {
method(input: string): number { ... }
}
class Bar {
method(input: string): number { ... }
}
let foo: Foo = new Bar(); // Okay.

Here you can see a pseudo-example of a structural type system passing when you’re trying to put a Bar where a Foo is required because their structure is exactly the same.

However, as soon as you change the shape it will start to cause errors.

class Foo {
method(input: string): number { ... }
}
class Bar {
method(input: string): boolean { ... }
}
let foo: Foo = new Bar(); // ERROR!!

It can get a little bit more complicated than this.

We’ve demonstrated both nominal and structure typing of classes, but there are also other complex types like objects and functions which can also be either nominal or structural. Even further, they can be different within the same type system (most of the languages listed before has features of both).

For example, Flow uses structural typing for objects and functions, but nominal typing for classes.

Functions

type FuncType = (input: string) => number;
function func(input: string): number { ... }
let test: FuncType = func; // Okay.

Here you can see a function type alias and an actual function with the same input and output. These have the same structure and are equivalent in Flow.

Objects

type ObjType = { property: string };
let obj = { property: "value" };
let test: ObjType = obj;

Here you can see an object type alias and an actual object with the same properties, only the values are different: string and “value”. Because “value” is a type of string, it is a subtype (it is a more specific type of string). Which means that our obj object is a subtype of ObjType.

This has nothing to do with the names (notice that there is nothing explicitly saying that obj is a subtype of ObjType) and everything to do with the structure of both. Since object types in Flow are covariant (meaning they accept subtypes) this passes the type checker.

Classes

class Foo { method(input: string): number {...} }
class Bar { method(input: string): number {...} }
let test: Foo = new Bar(); // ERROR!!

Here you can see our same two classes from before that have the same structure. In Flow these are not equivalent as Flow uses nominal typing for classes.

If you wanted to use a class structurally you could do that by mixing them with objects as interfaces:

type Interface = {
method(value: string): number;
};
class Foo { method(input: string): number {...} }
class Bar { method(input: string): number {...} }
let test: Interface = new Foo(); // Okay.
let test: Interface = new Bar(); // Okay.

The design decision in Flow around mixing nominal and structural typing was chosen based on how objects, functions, and classes are already used in JavaScript.

The JavaScript language is a bunch of object-oriented ideas and functional ideas mixed together. Developer’s usage of JavaScript tends to be mixed as well. Classes (or constructor functions) being the more object-oriented side and functions (as lambdas) and objects tend to be more on the functional side, developers use both simultaneously.

Object oriented languages tend to be more nominally typed (C++, Java, Swift), while functional languages tend to be more structurally typed (OCaml, Haskell, Elm).

When someone writes a class, they are declaring a thing. This thing might have the same structure as something else but they still serve different purposes. Imagine two component classes that both have render() methods, these components could still have totally different purposes, but in a structural type system they’d be considered exactly the same.

Flow chooses what is natural for JavaScript, and the end result is a very nice experience that does what you expect it to.