Nominal And Structural Typing

Nominal And Structural Typing

One of the core responsibilities of a type system is to decide if two given types are type compatible, or if a type is a subtype of another type. N4JS provides support for different strategies of checking whether two types are compatible, namely nominal (as known from Java) and structural typing (as known from TypeScript). Additionally it proivdes certain variations of structural typing to support typical usages in ECMAScript.

A type T1 is compatible with a type T2 if, roughly speaking, a value of type T1 may be used as if it were a value of type T2. Therefore, if type T1 is compatible to type T2, a value that is known to be of type T1 may, for example, be assigned to a variable of type T2 or may be passed as an argument to a function expecting a value of type T2. There are two major classes of type systems that differ in how they decide on type compatibility:

  • Nominal type systems.

  • Structural type systems.

Since N4JS provides both forms of typing, we briefly introduce each approach in the coming sections before we show how they are combined in N4JS.

Nominal Typing

In a nominal, or nominative, type system, two types are deemded to be the same if they have the same name and a type T1 is deemed to be a (immediate) subtype of a type T2 if T1 is explicitly declared to be a subtype of T2.

In the following example, Employee is a subtype of Person because it is declared as such using keyword "extends" within its class declaration. Conversely, Product is not a subtype of Person because it lacks such an "extends" declaration.

class Person {
    public name: string;
}

class Employee extends Person {
    public salary: number;
}

class Manager extends Employee { }

class Product {
    public name: string;
    public price: number;
}

The subtype relation is transitive and thus Manager is not just a subtype of Employee but also of Person. Product is not a subtype of Person, although it provides the same members.

Most mainstream object-oriented languages use nominal subtyping, for example C++, C#, Java, Objective-C.

Structural Typing

In a structural type system, two types are deemed the same if they are of the same structure. In other words, if they have the same public fields and methods of compatible type/signature. Similarly, a type T1 is deemed a subtype of a type T2 if and only if T1 has all public members (of compatible type/signature) that T2 has (but may have more).

In the example from the previous section, we said Product is not a (nominal) subtype of Person. In a structural type system, however, Product would indeed be deemed a (structual) subtype of Person because it has all of Person's public members of compatible type (only field name" in this case). The opposite is, in fact, not true: Person is not a subtype of Product because it lacks Product's field price.

Comparison

Both classes of type systems have their own advantages and proponents. Nominal type systems are usually said to provide more type safety and better maintainability whereas structual typing is mostly believed to be more flexible. As a matter of fact, nominal typing is structural typing extended with an extra relation explicitly declaring the subtype relation (like the extends clause). So the real question is: What are the advantages and disadvantages of such an explicit relation?

Let’s assume you want to provide a framework or library as follows:

export public interface Identifiable {
    public get name(): string

    static check(identifiable: Identifiable): boolean {
        return identifiable.name != 'anonymous';
    }
}
class A {

    public get name(): string { return 'John'; }
}
import { Identifiable } from 'Identifiable';

class A implements Identifiable {
    @Override
    public get name(): string { return 'John'; }
}

A client may use these classes as follows:

Identifiable.check(new A());

This first example is only to demonstrate the conceptual differences. The structural variant would cause an error in N4JS.

Maintainability

Everything works fine. But maybe you consider to rename name to id. Assume you have thousands of classes and interfaces. You start by renaming the function in the interface:

export public interface Identifiable {
    public get id(): string
  // …
}

With structural typing, you won’t get any errors in your framework. You are satisfied with your code and ship the new version. Alas! The client code will no longer work as you have forgotten to accordingly rename the function in class A.

With nominal typing, you would have gotten errors in your framework code already ("Class A must implement getter id." and "The getter name must implement a getter from an interface."). Instead of breaking the code on the client side only, you find the problem in the framework code. In large systems, this can be a complete lifesaver. Without this strict validation, you probably wouldn’t dare to refactor your framework. Of course, you may still break the client code, but even then it is much easier to pinpoint the problem.

Flexibility

Given the same code as in the previous example, assume that the client code also uses another framework providing a class Person as in the very first example.

With structural typing, it is no problem to use the Person class with the function check since the Person class provides a data field name. So the code inside the function would work perfectly when called with an instance of Person.

This won’t work with nominal typing though. Since Person does not explicitly "implement" Identifiable, there is no chance to call function check. This can be quite annoying, particularly if the client can change neither your framework nor the person framework.

Combination of Nominal and Structural Typing in N4JS

Because both classes of type systems have their advantages and because structural typing is particularly useful in the context of a dynamic language ecosystem as that of JavaScript, N4JS provides both kinds of typing and aims to combine them in a seamless way.

N4JS uses nominal typing by default, but allows to switch to structural typing by way of special type constructors using the tilde symbol. The switch can be done with either of the following:

  • Globally when defining a type. This then applies to all uses of the type throughout the code, referred to asdefinition-site structural typing

  • Locally when referring to an existing nominal type, referred to as use-site structural typing.

If we combine the above examples, we can use nominal and structural typing in N4JS as follows:

export public interface Identifiable {
    public get name(): string

    static check(identifiable: ~Identifiable): boolean {
        return identifiable.name != 'anonymous';
    }
}

class A implements Identifiable {
    @Override public get name(): string { return 'John'; }
}

For the argument of method "check" we use a (use-site) structural type by prefixing the type reference with a ~ (tilde), which means we are allowed, in the last line, to pass in an instance of Product even though Product is not a nominal subtype of Identifiable.

This way, N4JS provides the advantages of nominal typing (which is the default form of typing) while granting many of the advantages of structural typing if the programmer so chooses. Additionally, if you would rename name to id, the tilde would tell you that there may be client code calling the method with a structural type.

The full flexibility of a purely structurally typed language, however, cannot be achieved with this combination. For example, the client of an existing function that is declared to expect an argument of a nominal type N is confined to nominal typing. They cannot choose to invoke this function with an argument that is only a structural subtype of N (it would be a compile time error). This would possibly be exactly the intention of the framework author in order to enable easier refactoring later. This is comparable to access modifiers which serve the same purpose.

Field Structural Typing

N4JS provides some variants of structural types. Usually two structural types are compatible, if they provide the same properties, or in case of classes, public members. In ECMAScript we often only need to access the fields. In N4JS, we can use ~~ to refer to the so called "field structural type". Two field structural types are compatible, if they provide the same public fields - methods are ignored in these cases. Actually, N4JS can do even more. There are several modifiers to further filter the properties or members to be considered: ~r~ only considers getters or data fields, w only setters and data fields. ~i~ is used for initializer parameters: For every setter or (non-optional) data field in the type, the i -type needs to provide at least a getter (or readable data field).

Quick Links