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 num
There isn't a way to do this.
A suggestion tracking this on the GitHub site is Units of Measure.
In a future release, you'll be able to use type
to define alternate names for the primitives, but these will not have any checking associated with them:
type lat = number;
type lon = number;
var x: lat = 43;
var y: lon = 48;
y = 'hello'; // error
x = y; // No error
Actually there is a way to achieve what you want to achieve, but it's a bit tricky, has some limitations and could be completely unreasonable to a person who sees that code for the first time, so treat it as a curiosity rather than actual implementation ;)
Okay, so let's go. First, we need to create a "subclass" of Number
. The problem is, that lib.d.ts actually declares Number
as an interface, not a class (which is reasonable - no need to implement methods, browser takes care of that). So we have to implement all the methods declared by the interface, thankfully we can use existing implementation of declared var Number
.
class WrappedNumber implements Number {
//this will serve as a storage for actual number
private value: number;
constructor(arg?: number) {
this.value = arg;
}
//and these are the methods needed by Number interface
toString(radix?: number): string {
return Number.prototype.toString.apply(this.value, arguments);
}
toFixed(fractionDigits?: number): string {
return Number.prototype.toFixed.apply(this.value, arguments);
}
toExponential(fractionDigits?: number): string {
return Number.prototype.toExponential.apply(this.value, arguments);
}
toPrecision(precision: number): string {
return Number.prototype.toPrecision.apply(this.value, arguments);
}
//this method isn't actually declared by Number interface but it can be useful - we'll get to that
valueOf(): number {
return this.value;
}
}
There you go, we created a type WrappedNumber
which behaves just like number type. You can even add two WrappedNumber
s - thanks to the valueOf()
method. 2 limitations here, however: first, you need to cast variables to perform this operation. Second: the result will be a regular number
, so it should be again wrapped afterwards. Let's look at an example of addition.
var x = new WrappedNumber(5);
var y = new WrappedNumber(7);
//We need to cast x and y to <any>, otherwise compiler
//won't allow you to add them
var z = <any>x + <any>y;
//Also, compiler now recognizes z as of type any.
//During runtime z would be a regular number, as
//we added two numbers. So instead, we have to wrap it again
var z = new WrappedNumber(<any>x + <any>y); //z is a WrappedNumber, which holds value 12 under the hood
And here comes the most, in my opinion, tricky part. We now create 2 classes, Latitude
and Longitude
which will inherit from WrappedNumber
(so that they behave as numbers)
class Latitude extends WrappedNumber {
private $;
}
class Longitude extends WrappedNumber {
private $;
}
What the heck? Well, TypeScript uses duck typing when comparing types. Which means that two different types are considered to be "compatible" (and therefore assignable to itselves, i.e you can assign variable of one type to a value of other) when they have the same set of properties. And the solution is really simple: add a private member. This private member is pure virtual, it's not used anywhere and won't be compiled. But it makes TypeScript think that Latitude
and Longitude
are completely different types and, which we are interested in more, won't allow to assign variable of type Longitude
to that of type Latitude
.
var a = new Latitude(4);
var b: Longitude;
b = a; //error! Cannot convert type 'Latitude' to 'Longitude'
Voila! That's what we wanted. But the code is messy and you need to remember to cast types, which is really inconvenient, so don't use that actually. However, as you see, it's possible.
Based on Lodewijk Bogaards answer
interface Casted extends Number {
DO_NOT_IMPLEMENT
toManipulate: { castToNumberType:numberType, thenTo: number }
}
interface LatitudeNumber extends Casted {
LatitudeNumber
}
interface LongitudeNumber extends Casted {
LongitudeNumber
}
type numberType = number | Casted
var lat = <LatitudeNumber><numberType>5
function doSomethingStupid(long: LongitudeNumber,lat: LatitudeNumber) {
var x = <number><numberType>long;
x += 25;
return { latitude:lat, longitude:<LongitudeNumber><numberType>x }
}
var a = doSomethingStupid(<LongitudeNumber><numberType>3.067, lat)
doSomethingStupid(a.longitude,a.latitude)
I think doing the direct cast keeps the intention of a nominal type clear, the numberType Type unfortunately is needed because of a strange design descision where casting to number or Number still won't allow additions. The transpiled javascript is very simple with no boxing:
var lat = 5;
function doSomethingStupid(long, lat) {
var x = long;
x += 25;
return { latitude: lat, longitude: x };
}
var a = doSomethingStupid(3.067, lat);
doSomethingStupid(a.longitude, a.latitude);
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!
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/