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?
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 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'
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 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 .
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/
Here is a simple way to achieve this:
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); } }
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
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With