Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create 2 incompatible number-like types in TypeScript?

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.

like image 387
Stack Tracer Avatar asked Jan 02 '18 02:01

Stack Tracer


People also ask

What is Typings in TypeScript?

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.

How do I assign a number in TypeScript?

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.

What is the difference between number and number in TypeScript?

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.


1 Answers

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 numbers. 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

like image 73
jcalz Avatar answered Oct 22 '22 13:10

jcalz