Nominal Typing

The TypeScript type system is structural and this is one of the main motivating benefits. However, there are real-world use cases for a system where you want two variables to be differentiated because they have a different type name even if they have the same structure. A very common use case is identity structures (which are generally just strings with semantics associated with their name in languages like C#/Java).

There are a few patterns that have emerged in the community. I cover them in decreasing order of personal preference:

Using literal types

This pattern uses generics and literal types:

type Id < T extends string > = { type : T , value : string , } ​ type FooId = Id < 'foo' > ; type BarId = Id < 'bar' > ; ​ const createFoo = ( value : string ) : FooId => ( { type : 'foo' , value } ) ; const createBar = ( value : string ) : BarId => ( { type : 'bar' , value } ) ; ​ let foo = createFoo ( 'sample' ) let bar = createBar ( 'sample' ) ; ​ foo = bar ; foo = foo ;

Advantages No need for any type assertions

Disadvantage The structure {type,value} might not be desireable and need server serialization support



Using Enums

​Enums in TypeScript offer a certain level of nominal typing. Two enum types aren't equal if they differ by name. We can use this fact to provide nominal typing for types that are otherwise structurally compatible.

The workaround involves:

Creating a brand enum.

Creating the type as an intersection ( & ) of the brand enum + the actual structure.

This is demonstrated below where the structure of the types is just a string:

enum FooIdBrand { _ = "" } ; type FooId = FooIdBrand & string ; ​ enum BarIdBrand { _ = "" } ; type BarId = BarIdBrand & string ; ​ var fooId : FooId ; var barId : BarId ; ​ fooId = barId ; barId = fooId ; ​ fooId = 'foo' as FooId ; barId = 'bar' as BarId ; ​ var str : string ; str = fooId ; str = barId ;

Note how the brand enums, FooIdBrand and BarIdBrand above, each have single member () that maps to the empty string, as specified by ``{ = "" } . This forces TypeScript to infer that these are string-based enums, with values of type string , and not enums with values of type number . This is necessary because TypeScript infers an empty enum ( {} ) to be a numeric enum, and as of TypeScript 3.6.2 the intersection of a numeric enum and string is never``.

Using Interfaces

Because numbers are type compatible with enum s the previous technique cannot be used for them. Instead we can use interfaces to break the structural compatibility. This method is still used by the TypeScript compiler team, so worth mentioning. Using _ prefix and a Brand suffix is a convention I strongly recommend (and the one followed by the TypeScript team).

The workaround involves the following:

adding an unused property on a type to break structural compatibility.

using a type assertion when needing to new up or cast down.

This is demonstrated below: