Previous: , Up: Hacking   [Contents][Index]


10.4 TypeScript Migration

There isn’t much here yet. Maybe you can help?

This section contains notes regarding a migration to TypeScript. It is intended to serve as a guide—it is not prescriptive.

10.4.1 Migrating Away From GNU ease.js

Liza was originally written in GNU ease.js. TypeScript now provides many features that ease.js was written to address, though not all (most notably traits).

Since ease.js was designed with JavaScript interoperability in mind, and TypeScript generates prototypes from classes, TypeScript classes serve as drop-in replacements under most circumstances. However, subtypes must be migrated at the same time as their parents, otherwise type checking in TypeScript cannot properly be performed. If this is a concern, type assertions can potentially be used to coerce types during a transition period in conjunction with ease.js’ Class.isA.

Often times you will need to reference a class or interface as a dependency before it has been migrated away from ease.js. To do this, create a corresponding .d.ts file in the same directory as the dependency. For example, if a class Foo is contained in Foo.js, create a sibling Foo.d.ts file. For more information, see Declaration Files in the TypeScript handbook.

ease.js implements stackable Scala-like traits. Traits are not provided by TypeScript. Traits will therefore have to be refactored into, for example, decorators or strategies.

10.4.2 Structural Typing

TypeScript implements structural typing, also called duck typing. This means that any two types sharing the same “shape” are compatible with one-another.

For classes, this can be mitigated by defining private members, which then ensures that compatible types are indeed subtypes.

Interfaces can be used in either the traditional OOP sense, or as a means to define the shape of some arbitrary object. Since interfaces do not define implementation details, the distinction isn’t important—it does not matter if we receive an instance of an object implementing an interface, or some object arbitrary that just happens to adhere to it.

In other instances where we want to distinguish between two values with otherwise compatible APIs, Nominal Typing below.

10.4.3 Nominal Typing

It is sometimes desirable to distinguish between two otherwise compatible types. Consider, for example, a user id and a Unix timestamp. Both are of type number, but it’s desirable to ensure that one is not used where another is expected.

TypeScript doesn’t directly support nominal typing, where compatibility of data types are determined by name. Liza uses a convention called “branding”, abstracted behind a NominalType generic (defined in src/types/misc.d.ts).

type UnixTimestamp = NominalType<number, 'UnixTimestamp'>;
type Milliseconds  = NominalType<number, 'Milliseconds'>;

function timeElapsed( start: UnixTimestamp, end: UnixTimestamp ): Milliseconds
{
    return end - start;
}

const start = <UnixTimestamp>1571325570000;
const end   = <UnixTimestamp>1571514320000;

// this is okay
const elapsed = timeElapsed( start, end );

// this is not, since elapsed is of type Milliseconds
timeElapsed( start, elapsed );

Figure 10.4: Example of nominal typing

Consider the example in Figure 10.4. Both UnixTimestamp and Milliseconds are a number type, but they have been defined in such a way that their names are part of the type definition. Not only does the compiler prevent bugs caused from mixing data, but nominal types also help to make the code self-documenting.

If you want to have self-documenting types without employing nominal typing, use type aliases.

There are no prescriptive rules for whether a type should be defined nominally.

In some cases, it’s useful to use nominal types after having validated data, so that the compiler can enforce that assumption from that point forward. This can be done using type assertions.

type PositiveInteger = NominalType<number, 'PositiveInteger'>;

const isPositiveInteger = ( n: number ): n is PositiveInteger => n > 0;

const lookupIndex<T>( arr: T[], i: PositiveInteger ): T => arr[ i ];

// untrusted input from the user
const user_input = readSomeValue();

if ( isPositiveInteger( user_input ) )
{
    // user_input is now of type PositiveInteger
    return lookupIndex( data, user_input );
}

Figure 10.5: Validating nominal types

In Figure 10.5 above, we only assume something to be a PositiveInteger after having checked its value. After that point, we can use TypeScript’s type system to ensure at compile time that we are only using positive integers in certain contexts.

Never cast values (e.g. using ‘<PositiveInteger>user_input’) when type predicates are provided, since that undermines type safety.


Previous: , Up: Hacking   [Contents][Index]