I've been trying to figure out how to create 2 mutually-incompatible number-like/integer types in TS.
For example, in the code below, height and weight are both number-like, but the concept of adding them together or treating them equivalently is nonsensical and should be an error.
type height = number; // inches
type weight = number; // pounds
var a: height = 68;
var b: weight = operateScale(); // Call to external, non-TS function, which returns a weight.
console.log(a+b); // Should be an error.
Is there a way to create 2 types which are both numbers, but are not compatible with each other?
EDIT: As mentioned in the comments section, this behavior appears to be analogous to haskell's newtype
behavior.
EDIT2: After several hours of poking the problem with a pointy stick, I managed to reach an answer, which I have posted below.
Typings is the simple way to manage and install TypeScript definitions. It uses typings. json , which can resolve to the Typings Registry, GitHub, NPM, Bower, HTTP and local files. Packages can use type definitions from various sources and different versions, knowing they will never conflict for users.
The Typescript also has the Number object. The Number object is created using the Number constructor i.e by using the new Number() keyword. You can check the type of a variable using the typeof keyword.
If you use Number in a place where a type is expected, TypeScript will not complain, because Number is an interface. If you use Number in a place where a value (something that exists in emitted code) is expected, TypeScript will not complain, because Number is also a global constructor.
The closest thing to a newtype
in TypeScript is to create a new "nominal" type (TypeScript doesn't have nominal types but there are workarounds like branding) and create a value constructor and a field accessor function which just uses type assertions in the implementation. For example:
interface Height {
__brand: "height"
}
function height(inches: number): Height {
return inches as any;
}
function inches(height: Height): number {
return height as any;
}
interface Weight {
__brand: "weight"
}
function weight(pounds: number): Weight {
return pounds as any;
}
function pounds(weight: Weight): number {
return weight as any;
}
const h = height(12); // one foot
const w = weight(2000); // one ton
The types Height
and Weight
(sorry, can't bring myself to give new types a lowercase name) are treated as distinct types by the compiler. The height()
function is a Height
value constructor (takes a number
and returns a Height
), and the inches()
function is its associated field accessor (takes a Height
and returns a number
), and weight()
and pounds()
are the analogous functions for Weight
. And all of those functions are just identity functions at runtime. So JavaScript treats them as pure numbers with a bit of function-call overhead that's hopefully optimized away by a good compiler, and if you're really worried about that overhead, you can do the assertions yourself:
const h = 12 as any as Height;
const w = 2000 as any as Weight;
Now you have distinct named types you can use so you don't accidentally use a Height
where a Weight
is needed or vice versa. But, just like with a newtype
, the compiler will not treat these as a number
. Yes, you could make Height
and Weight
subtypes of number
(via intersection types), but that's probably a mistake: the arithmetic operators like +
are able to operate on number
values and if both h
and w
are subtypes of number
, then h + w
will not be an error. And if h
and w
are not subtypes of number
, then h + h
will be an error. And you can't change that, since TypeScript does not let you alter the type declarations of operators the way it does with functions. I prefer to prevent both h + h
and h + w
from compiling, so the Height
and Weight
types are not number
s. Instead, let's create our own add()
function that behaves how you want:
type Dimension = Height | Weight;
function add<D extends Dimension>(a: D, b: D): D {
return ((a as any) + (b as any)) as any;
}
The add()
function accepts either two Height
or two Weight
parameters, and returns a value of the same type. Actually, with the above it's still possible to pass in something like Height | Weight
as D
, so if you're really serious about locking it down you can use overloads instead:
function add(a: Height, b: Height): Height;
function add(a: Weight, b: Weight): Weight;
function add(a: any, b: any): any {
return a+b;
}
And, behold:
const twoH = add(h, h); // twoH is a Height
const twoW = add(w, w); // twoW is a Weight
const blah = add(h, w); // error, Height and Weight don't mix
So we're almost done. For your external measureScale()
function you'd just declare the return type to be a Weight
:
declare function measureScale(): Weight;
var a = height(68);
var b = measureScale();
And verify the intended result:
console.log(add(a,b)); // err
console.log(add(a,a)); // okay
Playground link to code
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