Typescript II - Nominal and Structural Typing

Here we look at two closely related concepts - the behaviour of Typescript’s type system, and how we can get the type of an object at runtime. We dive into structural typing, nominal typing, Typescript’s value and type spaces, classes, and enums.

Link to this section Structural Typing

Typescript is a structurally typed language. This means that types are identified by their shape rather than name or alias. We can see this demonstrated below. Even though our sayName function expects a Person the Typescript compiler is quite happy to accept a Pet.

type Person = {
    name: string;
    weight: number;
}

type Pet = {
    name: string;
    weight: number;
}

const basil: Person = { name: "Basil Exposition", weight: 65.4 };
const bigglesworth: Pet = { name: "Mr Bigglesworth", weight: 5.7 };

const sayName = (person: Person) => console.log(person.name);

sayName(basil);
// Basil Exposition
sayName(bigglesworth);
// Mr Bigglesworth

This makes more sense when we see how the Javascript code that is produced.

const basil = { name: "Basil Exposition", weight: 65.4 };
const bigglesworth = { name: "Mr Bigglesworth", weight: 5.7 };

const sayName = (person) => console.log(person.name);

sayName(basil);
sayName(bigglesworth);

sayName’s only condition is that whatever is passed to it has a property called name. This is duck typing, if it walks like a duck and it quacks like a duck, then it must be a duck, and follows the style you’d use to write typical Javascript or Python code.

Languages such as C, C#, and Java use nominal typing. The C# equivilent of the above Typescript code would not compile.

Link to this section Structural Typing Not So Fast

The danger of structural typing is accidentally being able to interchangeably use two different type declarations which share the same structure. It’s quite common in languages like F# (a nominally typed language) to alias base types to gain additional safety at compile time.

type PersonId = int
type PetId = int

let deletePerson (id: PersonId) = 
    ...

In a structurally typed language any int could be passed to the deleteUser function. This is definitely not what we want and goes against setting up the types in the first place. Further down we’ll see how we can safely do this in Typescript.

Link to this section But I Want To Know the Type at Runtime

In Part I we saw that type checking is done at compile time. The Typescript compiler then erases the types and generates the Javascript code to run. So at runtime how can we discriminate between the type of the object that is being passed? That snippet above shows there is no Person symbol anywhere in the generated code.

Coming from other programming languages we might expect we can do a check like so

console.log(basil instanceof Person)
// ReferenceError: Person is not defined

But we can’t, since there is no Person symbol in the value space. Another idea would be to check the type

console.log(typeof basil)
// object

At runtime basil is just an object, this follows along with the Javascript that was generated above. This illustrates the concept of the type space and value space. Our code has symbols, these are everything which we’ve assigned a name: types, classes, variables, constants, functions, etc.

When we write code we’re actually putting creating the symbol within either the type space or value space (there are a couple of cases where the symbol is created in both, we’ll get to that). Repeating the code from above..

type Person = {
    name: string;
    weight: number;
}

const basil: Person = { name: "Basil Exposition", weight: 65.4 };

This code creates the symbol Person in the type space and the symbol basil in the value space. A simple rule of thumb is anything that comes after a : is in the type space and everything else is in the value space. Where things start getting difficult is that the behaviour of some Typescript operators depends on the space you call it. For example using typeof in the type space lets us create a new type based on the structure of a constant object. You can see how this starts being useful in a structural type system.

const basil = { name: "Basil Exposition", weight: 65.4 };

type OtherPersonType = typeof basil;
/* type OtherPersonType = {
 *    name: string;
 *    weight: number;
 */ }

But we can also use typeof in the context of the value space. When this code is executed at runtime it returns the Javascript type of the symbol.

console.log(typeof basil);
// object

It’s important to understand that the type space and value space are completely different. We can see how that at runtime, while in the value space, there is no way to get the name of the Typescript type. We’ll get onto that later, but here’s a short detour to look at a couple of constructs which exist in both type and value spaces.

Link to this section Classes and Enums Confuse Things Further

We know that our type and interface symbols exist only in the type space, that is we cannot access them in the value space (ie. at runtime). Somewhat confusingly class and enum symbols are in both the type space and value space. For different reasons.

Typescript classes are just standard Javascript classes. Class symbols appear in both the type space (so we can do things like generics) as well as the value space, where they are the Javascript class symbol. So if we just declared our data types as a class instead of a type then we can differentiate them at runtime using the instanceof CPerson line.

class CPerson {

    name: string;
    weight: number;

    constructor(name: string, weight:number) {
        this.name = name;
        this.weight = weight;
    }
}

const cbasil: CPerson = new CPerson("Basil Exposition", 65.4);

console.log(typeof cbasil);
// object
console.log(cbasil instanceof CPerson);
// true

Enums also exist in both the type and value spaces.

Unlike type and interface declarations, a Typescript enum will generate Javascript that’s included at runtime.

enum Thoughts {
    Baseball,
    ColdShowers,
}

The Javascript object that’s generated.

"use strict";
var Thoughts;
(function (Thoughts) {
    Thoughts[Thoughts["Baseball"] = 0] = "Baseball";
    Thoughts[Thoughts["ColdShowers"] = 1] = "ColdShowers";
})(Thoughts || (Thoughts = {}));

Link to this section Nominal Typing in Typescript

We’re still looking for a way to be able to identify the Typescript type of an interface or type at runtime. We’ve seen we can do that with classes and enums, however neither fit our use case.

Even though the Typescript type system uses structural typing we can introduce a sort of nominal type. We’ll do this in such a way that it’s possible to discriminate the type of the type or interface at runtime. We do this by just adding a string literal to the type definition.

type Person = {
    _type: "Person";
    name: string;
    weight: number;
}

type Pet = {
    _type: "Pet";
    name: string;
    weight: number;
}

const basil: Person = { name: "Basil Exposition", weight: 65.4, _type: "Person" };
const bigglesworth: Pet = { name: "Mr Bigglesworth", weight: 5.7, _type: "Pet" };

const sayName = (person: Person) => console.log(person.name);

sayName(basil);
// Basil Exposition
sayName(bigglesworth);
/* Compile time error

   Argument of type 'Pet' is not assignable to parameter of type 'Person'.
   Types of property '_type' are incompatible.
   Type '"Pet"' is not assignable to type '"Person"'.(2345)
 */

That error makes sense. Typescript’s structural typing can see that Person and Pet differ by the string literal defined in _type. Because this _type property will be included on the Javascript objects we construct we can use it at runtime to discriminate between the types.

const sayTypeAndName = (thing: Person | Pet): void => {
    switch (thing._type) {
        case "Person":
            console.log("Person: " + thing.name);
            break;
        case "Pet":
            console.log("Pet: " + thing.name);
            break;
    }
}

sayTypeAndName(basil);
// Person: Basil Exposition

sayTypeAndName(bigglesworth);
// Pet: Mr Bigglesworth

Link to this section Include a _type Property Everywhere?

Adding a _type property to all of our types (also called tagging or branding) seems like a bit of a hack. It is! Although it’s the best way we’ve got at the moment. The Typescript language itself uses the same approach.

The Typescript project does have nominal typing support on the roadmap. There’s an active thread started in 2014 which discusses different implementation options and the use cases for nominal types.


Related Posts