# Structural vs. Nominal Type Systems

Alex Woods

June 15, 2022

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.

``````// TypeScript
type Country = {
name: string;
population: number;
};

type State = {
name: string;
population: number;
};

// a country can be assigned to a state
const x: Country = { name: "Sweden", population: 4 };
const sweden: State = x;

// we can pass a state to an argument of type Country
function travel(country: Country) {
console.log(`We are going to \${country.name}`);
}

const norway: State = { name: "Norway", population: 5 };
travel(norway); // We are going to Norway
``````

But structural typing can have problems — sometimes slippery boundaries are not ideal.

``````// TypeScript
// Don't do this
type Inches = number;
type Centimeters = number;

// function definition far far away
function calculateSomething(distance: Centimeters) {
/* todo */
}

const distance: Inches = 5;
// the linter: that looks great to me! 😈
calculateSomething(distance);
``````

You can mitigate this by giving each type a "brand".

``````// TypeScript
type Inches = {
_unit: "inches"; // this is a brand
value: number;
};
type Centimeters = {
_unit: "centimeters"; // this is a brand
value: number;
};

const distance: Inches = { value: 5, _unit: "inches" };

function calculateSomething(distance: Centimeters) {
/* todo */
}

// error: type Inches is not assignable to type Centimeters
calculateSomething(distance);
``````

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.

``````// Kotlin
data class Country(val name: String, val population: Int)
data class State(val name: String, val population: Int)

val x = Country(name="Sweden", population=4)
// error: type mismatch: inferred type is Country but State was expected
val sweden: State = x

fun travel(country: Country) {
val name = country.name
print("We are going to \$name")
}

val norway = State(name="Norway", population=5)
// error: type mismatch: inferred type is State but Country was expected
travel(norway)
``````

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.

``````// TypeScript
type User = {
id: string;
};

type Auth0User = {
id: string;
auth0UserId: string;
};

function getUserInfoFromDatabase(user: User) {}
function getUserInfoFromAuth0(auth0User: Auth0User) {}

const michael: Auth0User = { id: "239ruepi", auth0UserId: "23jlkj23" };
// no problem
getUserInfoFromDatabase(michael);

const idris: User = { id: "2u3r0wfj" };
// error: Argument of type User is not assignable to parameter of type Auth0User
getUserInfoFromAuth0(idris);
``````

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.

``````// Kotlin
open class User(val id: String)
class Auth0User(id: String, auth0UserId: String): User(id) // declare subtype relationship

fun getUserInfoFromDatabase(user: User) {}
fun getUserInfoFromAuth0(auth0User: Auth0User) {}

val michael = Auth0User(id="239ruepi", auth0UserId="23jlkj23")
getUserInfoFromDatabase(michael) // all good

val idris = User(id="2u3r0wfj")
// error: type mismatch: inferred type is User but Auth0User was expected
getUserInfoFromAuth0(idris)
``````

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.

``````// Go
package main

type Country struct {
name       string
population int
}

type State struct {
name       string
population int
}

func main() {
x := Country{name: "Sweden", population: 4}
var norway State
// error: cannot use x as State value in assignment
norway = x
}
``````

It's the same for passing arguments to a function.

``````func travel(country Country) {
fmt.Printf("We are going to %s", country.name)
}

norway := State{name: "Norway", population: 5}
// error: Cannot use variable norway (of type State) as Country value
travel(norway)
``````

So, nominal, right? Not quite.

If we don't use a named type in our function definition, then it matches based on structure.

``````func travel(country struct {
name       string
population int
}) {
fmt.Printf("We are going to %s", country.name)
}

// can pass variables with type Country or State into this
``````

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.

``````// TypeScript
type Inches = {
_unit: "inches"; // this is a brand
value: number;
};
``````

That `_unit` brand is going nowhere at runtime, because we have to do this.

``````// TypeScript
const distance: Inches = { value: 5, _unit: "inches" };
``````

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

Want to know when I write a new article?

Get new posts in your inbox