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).