Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to create nominal types in TypeScript that extend primitive types?

Tags:

Say I have two types of numbers that I'm tracking like latitude and longitude. I would like to represent these variables with the basic number primitive, but disallow assignment of a longitude to a latitude variable in typescript.

Is there a way to sub-class the number primitive so that typescript detects this assignment as illegal? Someway to coerce nominal typing so that this code fails?

var longitude : LongitudeNumber = new LongitudeNumber(); var latitude : LatitudeNumber; latitude = longitude; // <-- type failure 

The answer to "How to extend a primitive type in typescript?" seems like it will put me in the right direction, but I am not sure how to extend that solution to create distinct nominal sub-types for different kinds of numbers.

Do I have to wrapper the primitive? If so, can I make it behave somewhat seamlessly like a normal number or would I have to reference a sub-member? Can I just somehow create a typescript compile-time number subclass?

like image 969
Ross Rogers Avatar asked Nov 07 '14 21:11

Ross Rogers


People also ask

Can you extend a type in TypeScript?

Use an intersection type to extend a type in TypeScript, e.g. type TypeB = TypeA & {age: number;} . Intersection types are defined using an ampersand & and are used to combine existing object types. You can use the & operator as many times as necessary to construct a type.

What is @types in TypeScript?

What is a type in TypeScript. In TypeScript, a type is a convenient way to refer to the different properties and functions that a value has. A value is anything that you can assign to a variable e.g., a number, a string, an array, an object, and a function. See the following value: 'Hello'

Is TypeScript nominal or structural?

TypeScript's type system is structural, which means if the type is shaped like a duck, it's a duck. If a goose has all the same attributes as a duck, then it also is a duck.

What is ?: In TypeScript?

What does ?: mean in TypeScript? Using a question mark followed by a colon ( ?: ) means a property is optional. That said, a property can either have a value based on the type defined or its value can be undefined .


2 Answers

You can approximate opaque / nominal types in Typescript using a helper type. See this answer for more details:

// Helper for generating Opaque types. type Opaque<T, K> = T & { __opaque__: K };  // 2 opaque types created with the helper type Int = Opaque<number, 'Int'>; type ID = Opaque<number, 'ID'>;  // works const x: Int = 1 as Int; const y: ID = 5 as ID; const z = x + y;  // doesn't work const a: Int = 1; const b: Int = x;  // also works so beware const f: Int = 1.15 as Int; 

Here's a more detailed answer: https://stackoverflow.com/a/50521248/20489

Also a good article on different ways to to do this: https://michalzalecki.com/nominal-typing-in-typescript/

like image 51
bingles Avatar answered Oct 13 '22 15:10

bingles


Here is a simple way to achieve this:

Requirements

You only need two functions, one that converts a number to a number type and one for the reverse process. Here are the two functions:

module NumberType {     /**      * Use this function to convert to a number type from a number primitive.      * @param n a number primitive      * @returns a number type that represents the number primitive      */     export function to<T extends Number>(n : number) : T {         return (<any> n);     }      /**      * Use this function to convert a number type back to a number primitive.      * @param nt a number type      * @returns the number primitive that is represented by the number type      */     export function from<T extends Number>(nt : T) : number {         return (<any> nt);     } } 

Usage

You can create your own number type like so:

interface LatitudeNumber extends Number {     // some property to structurally differentiate MyIdentifier     // from other number types is needed due to typescript's structural     // typing. Since this is an interface I suggest you reuse the name     // of the interface, like so:     LatitudeNumber; } 

Here is an example of how LatitudeNumber can be used

function doArithmeticAndLog(lat : LatitudeNumber) {     console.log(NumberType.from(lat) * 2); }  doArithmeticAndLog(NumberType.to<LatitudeNumber>(100)); 

This will log 200 to the console.

As you'd expect, this function can not be called with number primitives nor other number types:

interface LongitudeNumber extends Number {     LongitudeNumber; }  doArithmeticAndLog(2); // compile error: (number != LongitudeNumber) doArithmeticAndLog(NumberType.to<LongitudeNumber>(2)); // compile error: LongitudeNumer != LatitudeNumber 

How it works

What this does is simply fool Typescript into believing a primitive number is really some extension of the Number interface (what I call a number type), while actually the primitive number is never converted to an actual object that implements the number type. Conversion is not necessary since the number type behaves like a primitive number type; a number type simply is a number primitive.

The trick is simply casting to any, so that typescript stops type checking. So the above code can be rewritten to:

function doArithmeticAndLog(lat : LatitudeNumber) {     console.log(<any> lat * 2); }  doArithmeticAndLog(<any>100); 

As you can see the function calls are not even really necessary, because a number and its number type can be used interchangeably. This means absolutely zero performance or memory loss needs to be incurred at run-time. I'd still strongly advise to use the function calls, since a function call costs close to nothing and by casting to any yourself you loose type safety (e.g doArithmeticAndLog(<any>'bla') will compile, but will result in a NaN logged to the console at run-time)... But if you want full performance you may use this trick.

It can also work for other primitive like string and boolean.

Happy typing!

like image 39
Lodewijk Bogaards Avatar answered Oct 13 '22 14:10

Lodewijk Bogaards