0

In TypeScript I am able to set a type like so:

type mode = 1 | 2 | 3 | 4 | 5;

Doing this, restricts the allowed mode values the numbers 1,2,3,4,5.

I am trying to work out if / how it is possible to set conditions on types, so instead of stating values such as above, am I able to replace this with something like:

type mode >= 0 && mode <= 5

If this is not possible in a type, is it possible using an interface. I simply wish to check values against a condition, instead of a number of literal values as detailed above.

Barry
  • 13
  • 2
  • No, there is no such functionality in TypeScript, see [microsoft/TypeScript#26382](https://github.com/microsoft/TypeScript/issues/26382) for the feature request. You can use sketchy recursion to generate union types without having to write them out, but it's brittle and limited and much longer to set up than the 17 keystrokes necessary to write `1 | 2 | 3 | 4 | 5`. What is your use case? – jcalz Mar 25 '21 at 18:44
  • I believe this will help you https://stackoverflow.com/questions/65307438/how-to-define-properties-in-a-typescript-interface-with-dynamic-elements-in-the and this https://catchts.com/range-numbers – captain-yossarian from Ukraine Mar 25 '21 at 18:51
  • @jcalz so the code in my question is simply for example purposes. I have a number of properties that I wish to perform conditional checks on for validation purposes. This can of course be performed elsewhere in my code, but it would be alot cleaner to do it this way. – Barry Mar 25 '21 at 18:54

2 Answers2

1

There is no subtype of number in TypeScript which corresponds exactly to those values for which the following function returns true:

function validMode(mode: number): boolean {
    return mode > 0 && mode <= 5;
}
console.log(validMode(1), validMode(5), 
  validMode(2.5), validMode(Math.PI)); // true, true, true, true
console.log(validMode(0), validMode(6), 
   validMode(-1), validMode(Infinity)); // false, false, false, false

Note that numbers like 2.5 and Math.PI are accepted, since values in JavaScript for which typeof mode === "number" are double precision floating point numbers and are not confined to integers.

In order to support such a type, TypeScript would likely need one or both of the following missing features:

  • refinement types, where you can place conditions on a type in the form of a predicate; there is a feature request for this at microsoft/TypeScript#7599, but it doesn't look like there's any work being done there.

  • mathematical operations on numeric literal types, where you can actually write the predicate like ValidMode > 0 and the compiler will understand that this corresponds to either true or false depending on the literal type of ValidMode. There is a feature request for this at microsoft/TypeScript#26382, but again, I don't see any obvious work being done to support this.

So it's not possible in general.


Of course the original mode type you wrote would not accept arbitrary numbers between 1 and 5, but only integer values. Something more like this:

function validMode(mode: number): boolean {
  return Number.isInteger(mode) && mode > 0 && mode <= 5;
}
console.log(validMode(1), validMode(5)); // true, true
console.log(validMode(2.5), validMode(Math.PI)); // false, false

And, as you know, such a type can be represented, but only as an explicit union of a finite number of numeric literals:

type ValidMode = 1 | 2 | 3 | 4 | 5;

Practically speaking if a union has 25 or fewer members it will behave reasonably; if it has more than 25 but less than, say, 10,000 members, it will behave oddly in some circumstances (see microsoft/TypeScript#40803 for example), and if it has more than 10,000 members you are going to get close to or pass some hard limitations on union size (see this error message).

Missing from TypeScript is the concept of a "range type" where one could easily write

type ValidMode = Range<1, 5>; // or would that be Range<1, 6>? Inclusive/exclusive ‍♂️

There is an open feature request for this at microsoft/TypeScript#15480. There are workarounds involving recursion or generics and even template literal types, but none of them are both understandable and well-behaved. If you look at the linked issue you can find some recursive implementations that result in unions, which won't work if you want Range<0, 1.0e+6> or even Range<0, 50>. And using template literal types to parse strings is not something I'd suggest doing for anything but entertainment purposes.

So for now, this is also not possible.


A different approach entirely is to accept that the compiler cannot both easily and accurately represent your type. Instead, we would like to create a branded subtype of number called ValidMode. We can write a type guard function to perform a runtime check on its number input, and decide whether or not that input is a ValidMode. If so, we "brand" the number with a phantom property that marks it as valid. If not, we leave it alone. Then later you can write functions that only accept ValidMode and the compiler will enforce this:

type ValidMode = number & { __validMode: true };
function validMode(mode: number): mode is ValidMode {
  return Number.isInteger(mode) && mode > 0 && mode <= 5;
}
function onlyAcceptValidModes(mode: ValidMode) {
  console.log(mode.toFixed(2));
}

This makes it a bit harder to use ValidMode, since the compiler cannot verify that a numeric literal like 4 matches it:

onlyAcceptValidModes(4); // error, number is not assignable to ValidMode

Instead, you will need to perform the runtime validation test to get your value stamped with approval:

const someNumber = Math.random() < 0.5 ? 4 : 8;
onlyAcceptValidModes(someNumber); // error again

if (validMode(someNumber)) {
  onlyAcceptValidModes(someNumber) // okay
}

In this way you can simulate refinement types in TypeScript. Since the test only exists at runtime it can be as complicated as you'd like.


Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for the detailed answer. In terms of my problem, as mentioned the 0-5 is just an example. This could easily be any other logical check on a value, not neccesarily just checking the value of a number. Its clear howeever from your answer that what i need is not currently available out of the box. This is ok, it would just have been a nice to have and kept the code a little cleaner. Thanks again for the detailed answer, it cleared up a few questions I had and its very much appreciated. – Barry Mar 26 '21 at 06:14
0

You can get something close to what you want by using a user-defined type guard:

type ZeroToFive = number & {
  // Here you can add properties that should only 
  // exist if the number is between zero and five.
  example(): number;
};

function isZeroToFive(n: Number): n is ZeroToFive {
  return 0 <= n && n <= 5;
}

function foo(n: number) {
  if (isZeroToFive(n)) {
    console.log(n.example());
  }
  else {
    console.log(n); // Here n.example() is not allowed
  }
}

Playground

md2perpe
  • 3,372
  • 2
  • 18
  • 22