Typescript: Suggestion: Range as Number type

Created on 30 Apr 2017  ·  106Comments  ·  Source: microsoft/TypeScript

When defining a type one can specify multiple numbers separated by |.

type TTerminalColors = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;

Allow to specify number types as ranges, instead of listing each number:

type TTerminalColors = 0..15;
type TRgbColorComponent = 0..255;
type TUInt = 0..4294967295;

Maybe use .. for integers and ... for floats.

interface Math {
  random(): 0...1
}
In Discussion Suggestion

Most helpful comment

This idea can be expanded to characters, e.g. "b".."d" would be "b" | "c" | "d". It would be easier to specify character sets.

All 106 comments

This idea can be expanded to characters, e.g. "b".."d" would be "b" | "c" | "d". It would be easier to specify character sets.

I think this can be expanded and use syntax like and semantics like Haskell ranges.

| Syntax | Desugared |
|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| type U = (e1..e3) | type U = \| e1 \| e1+1 \| e1+2 \| ...e3 \|
The union is never if e1 > e3 |
| type U2 = (e1, e2..e3) | type U2 = \| e1 \| e1+i \| e1+2i \| ...e3 \|,
where the increment, i, is e2-e1.

If the increment is positive or zero, the union terminates when the next element would be greater than e3;
the union is never if e1 > e3.

If the increment is negative, the union terminates when the next element would be less than e3;
the union is never if e1 < e3. |

@panuhorsmalahti What if you specify "bb".."dd"?

@streamich

Maybe use .. for integers and ... for floats.

I really like the idea of generating integral types like this, but I don't see how floating point values could work.

@aluanhaddad Say probability:

type TProbability = 0.0...1.0;

@streamich so that type has a theoretically infinite number of possible inhabitants?

@aluanhaddad actually it would be far from infinite in IEEE floating point. It would have 1,065,353,217 inhabitants by my calculations.

0.0...1.0? JS uses IEEE double, that's 53 bits of dynamic range. If that were to be supported, ranges would have to be a first class type, desugaring that to a union would be beyond impractical.

@jcready indeed but, as @fatcerberus points out, realizing it as a union type would be prohibitively expansive.

What I was getting at, in a roundabout manner, was that this would introduce some notion of discrete vs. continuous types into the language.

realizing it as a union type would be prohibitively expansive.

@aluanhaddad Yes, but even specifying an unsigned integer as a union would be very expensive:

type TUInt = 0..4294967295;

This really needs some compelling use cases, because the implementation of unions today is completely unsuited to realizing unions this large. Something that would happen if you wrote something like this

type UInt = 0..4294967295;
var x: UInt = ......;
if (x !== 4) {
  x;
}

would be the instantiation of the union type 0 | 1 | 2 | 3 | 5 | 6 | 7 | ....

Perhaps it could only work against number literals. Any non-literal number values would have to be explicitly refined with greater/less than comparisons before being considered to inhabit the range. Integer ranges would also require an additional Number.isInteger() check. This should eliminate the need to generate actual union types.

@RyanCavanaugh Subtraction types? 🌞

Negative types, type negation.

Anything but a string:

type NotAString = !string;

Any number except zero:

type NonZeroNumber = number & !0;

@streamich subtraction types are covered by #4183

My use case is: I'd like to type a parameter as 0 or a positive number (it's an array index).

@RoyTinker I definitely think this would be cool but I don't know if that use case helps the argument.
An array is just an object and the ascending indexes are just a convention.

let a = [];
for (let i = 0; i > -10; i -= 1) {
  a[i] = Math.random() * 10;
}

so you ultimately still have to perform the same check

function withItem<T>(items: T[], index: number, f: (x: T) => void) {
  if (items[index]) {
    f(items[index]);
  }
}

It would be quite useful for defining types like second, minute, hour, day, month, etc.

@Frikki those units are on a confined enough interval that it is practical and prohibitively difficult to write them by hand.

type Hour =
   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
   | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23;

@aluanhaddad But no unsigned int:

type UInt = 0..4294967295;

meh, how about a type like this:

type Factorial<N extends number> = N > 2 ? Factorial<N - 1> * N : N;
type F1 = Factorial<1>; // 1
type F2 = Factorial<2>; // 1 | 2
type F3 = Factorial<3>; // 1 | 2 | 6
type FN = Factorial<number>; // 1 | 2 | 6 | ... 

Using * operator from @aleksey-bykov comment:

type char = 0..255;
type word = char ** 2;
type int = word ** 2;
type bigint = int ** 2;

@streamich Doubling the bit count does not correspond to multiplication by two, it's more like exponentation with 2 as the the exponent. It's still not correct, though, as you should not raise the upper bound, but the encodable numbers count. All in all, that's not a good definition strategy.

@streamich, some comments:

  • Using the termschar, word, etc. could be confusing since folks from other languages might not realize the difference between static definition and runtime behavior.
  • Your proposed syntax doesn't take the lower bound into account -- what if it is non-zero?
  • I'd be wary about co-opting the exponentiation operator for use in an ambient/type context, since it's already been added to ES2016.

let's just make the type system turing complete and enjoy the halt problem as we hit Ctrl + Shift + B

@aleksey-bykov surely you remember this fine issue 😀

@aluanhaddad

those units [time units] are on a confined enough interval that it is practical and prohibitively difficult to write them by hand.

https://github.com/Microsoft/TypeScript/issues/15480#issuecomment-349270853

Now, do that with milliseconds :wink:

Is this issue dead?

@Palid it is tagged with [Needs Proposal] so I doubt that.

As much fun as the discussion has been, most of us have failed to provide compelling real world use cases.

See https://github.com/Microsoft/TypeScript/issues/15480#issuecomment-324152700

__USE CASES:__

  1. You can define precise int types such that subset of TypeScript can be compiled to WebAssembly or some other target.

  2. Another use case is UInt8Array, Int8Array, Uint16Array, etc.., when you read or write data from those TypeScript could check for errors.

const ab = new ArrayBuffer(1e3);
const uint8 = new UInt8Array(ab);

uint8[0] = 0xFFFFFFFF; // TSError: Number too big!
  1. I've mentioned a few use cases in my OP.

  2. If you implement this, TypeScript community will come up with millions of use cases.

I've got a really funny usecase, that actually may be closer to anything else proposed here.
API takes an integer between some range (in my case 5-30) and we need to implement an SDK for that.
Manually writing underneath is tedious (though it can be kinda automated) for 25values, but what about hundreds or thousands?
type X = 5 | 6 | 7 | 8 | 9 | 10 ... | 30

@Palid That's the best and most straightforward case I've seen for this feature.

If a range like 5..30 was defined as syntactic sugar for 5 | 6 | 7 ... | 30 (or functions identically to it) I bet this would be an easy win. Here it would stand for a discrete range of integers.

Perhaps a continuous range (as opposed to discrete) could be notated by using a number with a period -- 5.0..30.0.

I was actually considering looking at Typescript to implement typing protection for
https://www.npmjs.com/package/memory-efficient-object

Having range typing would make it easier to spot potential memory overflow at type checking time

Seem like we don't really need union expansion but more like inclusive/exclusive range definition that just compare the range

type TTerminalColors = int & [0,15];
// int && v >= 0 && v <= 15

This would be very useful when controlling motors (using Johnny-Five, for instance), where the speed ranges from 0-255.

Another use case: I am implementing a Canvas-based drawing program using TypeScript and would like to have type checks on opacity (which must be a number between 0.0 and 1.0).

Just thinking... to implement this properly, you’d have to really go all out:

  • support runtime type guard functions
  • type narrowing for conditions like x <= 10
  • support for string | number ranges (since x == 5 is true for x === "5") in addition to number-only ranges
  • probably even support for a new int type (which would be a subtype of number), and support for int-only and string | int ranges. Also type narrowing for expressions like x|0

Great idea, but that would be a lot to cover!

Maybe we don't need runtime type guard. Instead we could make compile time guard all the way up

This would need elaborate branch prediction instead

Suppose

type TTerminalColors = int & [0,15];

function A(color: TTerminalColors):void
{

}

A(15); // OK
var x = 15;
A(x); // OK

function B(value: int) : void
{
    A(value); // ERROR!!!
    if(value >= 0 && value <= 15)
        A(value); // OK, because we check that it is in the range of TTerminalColors
}

function C(value: int) : void
{
    if(value < 0)
        value = 0;
    if(value > 15)
        value = 15;

    A(value); // OK, because is clamped. But maybe too hard to implemented
}

function ClampInt(value: int): TTerminalColors
{
    if(value >= 0 && value <= 15)
        return value; // Same as B(int)

    if(value > 15)
        return 15;

    return 0;
}

My use case:

export const ajax: (config: AjaxOptions) => void;

type Callback = Success | Error;
type Success = (data: any, statusText: string, xhr: XMLHttpRequest) => void;
type Error = (xhr: XMLHttpRequest, statusText: string) => void;

interface AjaxOptions {
  // ...
  statusCode: { [code: number]: Callback | Callback[] },
  // ...
}

It would be nice to be able to restrict the keys in the statusCode option such that at compile-time it can be determined if a status code corresponds to a success or error code:

interface AjaxOptions {
  // ...
  statusCode: {
    200..300: Success,
    400..600: Error
  },
  // ...
}

IMO this should be restricted to floats and integers and should not be implemented as a union, but rather a new range type. Then type checking would be as simple as:

if (val >= range.start && val < range.end) {
  return match;
} else {
  return no_match;
}

We could perhaps take a leaf out of Ruby's book and use .. for inclusive ranges ([start, stop]) and ... for non inclusive ranges ([start, stop)).

Another use case would be to type check you database records:

Another use case would be type checking Lat/Lon values.

This could be covered by the general validation mechanism of #8665 (not sure why it was closed as a duplicate):

type TTerminalColors (n: number) => Math.floor(n) == n && n >= 0 && n <= 15;
type TRgbColorComponent (n: number) => Math.floor(n) == n && n >= 0 && n <= 255;
type TUInt (n: number) => n >= 0 && n <= 0..4294967295;

Or taken with #4639, and assuming the unsigned integer type is defined as uint:

type TTerminalColors (n: uint) => n <= 15;
type TRgbColorComponent (n: uint) => n <= 255;

My use case is global coordinates. I want to type out latitude and longitude so they only fall within the specific ranges (-90 to 90 and -180 to 180).

Edit: lat and long have a negative range

My use case is the implementation of arrays of fixed size where size is a parameter.

For example, I want to define a type for arrays of strings of length 2.

let d: FixedSizeArray<2, string>;
d = [ 'a', 'b' ]; // ok
d = [ 'a' ]; // type error
d = [ 'a', 'b', 'c' ]; // type error
d[0] = 'a1'; // ok
d[1] = 'b1'; // ok
d[2] = 'c1' // type error

With the current version of TS it is possible to define something very close to the "spec" above. The main problem is member access: e.g. d[1] = 'b1' returns a type error even if it is correct. In order to avoid the error the list of all legal indices must be compiled by hand in the definition of FixedSizeArray, which is boring.

If we had a range operator similar to keyof operator, the following type definition should solve the problem.

type FixedSizeArray<U extends number, T> = {
    [k in range(U)]: T;
} & { length: U };

Where range(N) is a shortcut for range(0,N).

Given numerical literals (naturals) M, N with M < N,

type r = range(M, N); 

is equivalent to

type r = M | M+1 | ... | N-1

What might be more general is if we could have the ability to define predicate lambdas and use them as types. Example:

predicate mypredicate = (x) => x > 1 && x < 10 
let x: mypredicate = 11 // not ok
let x: mypredicate = 5 // ok

A pro would be low syntax complexity as Lambdas are already available all we need is the ability to use them as types, type checking is typescript specific anyway (keeping in mind the "superset to Javascript" philosophy)
A con is that complexity of the predicate will determine the performance of tooling to provide feedback.

Ensuring a number/character belongs to an arithmetic progressions could be a good common use-case:

// a is the starting element d is the difference between two elements and L is the last element
const belongsToAP = (a, d, L) => {
  return (x) => {
    if(x < a || x > L) return false
    let n = ((x-a)/d) + 1
    if(Number.isInteger(n)) return true
    return false
  }
}

this would allow us to do type checks like:
predicate belongsToMyAP = belongsToAP(1,1, 10)

let x : belongsToMyAP = 5 // ok
let y : belongsToMyAP = 7.2 // not ok

This can be expanded to characters as well.

@Kasahs Something similar to this has already been proposed in #8665.

Throwing my lot in with the "reflecting an API" use-case. A REST endpoint I'm writing a wrapper function for takes an integer in the range 1..1000 as an argument. It generates an error if the number does not satisfy that constraint.

So I'm writing a proposal for numeric ranges and I've run across this issue that I'm not sure how to deal with, so I'm throwing it out there for consideration.

Because the TypeScript compiler is written in TypeScript, it's possible to leverage the properties of the numeric operators to mutate range types.

// Syntax: x..y for an inclusive integer range.

let x: 0..10 = randomNumber(0, 10);
let y = x + 2; // Can deduce that y: 2..12.

This is fine when assigning to a new variable, but what about mutation?

let x: 0..10 = randomNumber(0, 10);
x += 2; // Error: 2..12 is not assignable to type 0..10 (upper bound is out of range).

This error would be techincally correct, but in practice it would be incredibly annoying to deal with. If we want to mutate a variable returned by a function with a range return type, we would always have to add a type assertion.

let x = randomNumber(0, 10) as number; // If randomNumber doesn't return a type assigable to number
// this will be an error, but it would still be annoying to have to sprinkle "as number"
// expressions everywhere.

And if we ignore it, we get the following unsoundness:

function logNumber0To10 (n: 0..10): void {
    console.log(n);
}

let x: 0..10 = randomNumber(0, 10);
x += 2; // Because we're ignoring mutations, x: 0..10, but the runtime value could be 11 or 12,
// which are outside the specified range...
logNumber0To10(x); // ...which means we lose type safety on this call.

A fix for this would be the ability to change the type of a variable after it's declared, so example 2 would just change the type of x to 2..12; but my first instinct is that would introduce too much overhead in the compiler and be confusing to users.

And what about user-defined functions and generics?

// How to define this return type, seeing as we can't do math in types?
function increment<L extends number, H extends number> (x: L..H): (L + 1)..(H + 1);

Any thoughts on how to deal with the above?

@JakeTunaley

what about mutation?

Why should mutation be different than any other assignment?

Here is my humble attempt at a proposal. This proposal also requests that other types be added.

  • Infinity type

    • Only has the value Infinity

  • -Infinity type

    • Only has the value -Infinity

  • NaN type

    • Only has the value NaN

  • double type

    • All number values in the range [-Number.MAX_VALUE, Number.MAX_VALUE] or, [-1.7976931348623157e+308, 1.7976931348623157e+308]

  • number is just Infinity|-Infinity|NaN|double
  • int type

    • A sub-type of double

    • All number values x in the range [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER] or [-9007199254740991, 9007199254740991] and Math.floor(x) === x

    • So, 3 and 3.0 would values of be an int type

  • "Finite literal" type

    • Examples are 1, 3.141, 45

    • May be sub-types of double or int

  • The "GtEq" type denoted (>= x)

    • Where x is a finite literal, Infinity, or -Infinity

  • The "LtEq" type denoted (<= x)

    • Where x is a finite literal, Infinity, or -Infinity

  • The "Gt" type denoted (> x)

    • Where x is a finite literal, Infinity, or -Infinity

  • The "Lt" type denoted (< x)

    • Where x is a finite literal, Infinity, or -Infinity


GtEq type; (>= x)

  • (>= Infinity) = Infinity
  • (>= -Infinity) = -Infinity|double|Infinity
  • Infinity is a sub-type of (>= [finite-literal])
  • (>= [finite-literal]) is a sub-type of double|Infinity
  • (>= NaN) = never
  • (>= int) = (>= -9007199254740991)
  • (>= double) = (>= -1.7976931348623157e+308)
  • (>= number) = number

Gt type; (> x)

  • (> Infinity) = never
  • (> -Infinity) = double|Infinity
  • Infinity is a sub-type of (> [finite-literal])
  • (> [finite-literal]) is a sub-type of double|Infinity
  • (> NaN) = never
  • (> int) = (> -9007199254740991)
  • (> double) = (> -1.7976931348623157e+308)
  • (> number) = number

LtEq type; (<= x)

  • (<= Infinity) = -Infinity|double|Infinity
  • (<= -Infinity) = -Infinity
  • -Infinity is a sub-type of (<= [finite-literal])
  • (<= [finite-literal]) is a sub-type of -Infinity|double
  • (<= NaN) = never
  • (<= int) = (<= 9007199254740991)
  • (<= double) = (<= 1.7976931348623157e+308)
  • (<= number) = number

Lt type; (< x)

  • (< Infinity) = -Infinity|double
  • (< -Infinity) = never
  • -Infinity is a sub-type of (< [finite-literal])
  • (< [finite-literal]) is a sub-type of -Infinity|double
  • (< NaN) = never
  • (< int) = (< 9007199254740991)
  • (< double) = (< 1.7976931348623157e+308)
  • (< number) = number

Range types

Note that while we can write things like (>= Infinity), (> number), etc.,
the resulting type is not a range type; they're just aliases for other types.

A range type is one of,

  • (>= [finite-literal])
  • (> [finite-literal])
  • (<= [finite-literal])
  • (< [finite-literal])

We allow syntax like (> number) and its like for use in generics.


Union GtEq/Gt types

When taking the union of two GtEq/Gt types, the type with "more" values is the result,

  • (>= [finite-literal-A]) | (>= [finite-literal-B]) = ...

    • If [finite-literal-A] >= [finite-literal-B], then the result is (>= [finite-literal-B])

    • Else, the result is (>= [finite-literal-A])

    • e.g. (>= 3) | (>= 5.5) = (>= 3) because (>= 3) is a super-type of (>= 5.5)

  • (>= [finite-literal-A]) | (> [finite-literal-B]) = ...

    • If [finite-literal-A] == [finite-literal-B], then the result is (>= [finite-literal-A])

    • If [finite-literal-A] > [finite-literal-B], then the result is (> [finite-literal-B])

    • Else, the result is (>= [finite-literal-A])

  • (> [finite-literal-A]) | (> [finite-literal-B]) = ...

    • If [finite-literal-A] >= [finite-literal-B], then the result is (> [finite-literal-B])

    • Else, the result is (> [finite-literal-A])

    • e.g. (> 3) | (> 5.5) = (> 3) because (> 3) is a super-type of (> 5.5)

Also,

  • (>= A|B) = (>= A) | (>= B)
  • (> A|B) = (> A) | (> B)

    • e.g. (> 4|3) = (> 4) | (> 3) = (> 3)

    • e.g. (> number|3) = (> number) | (> 3) = number | (> 3) = number

Union LtEq/Lt types

When taking the union of two LtEq/Lt types, the type with "more" values is the result,

  • (<= [finite-literal-A]) | (<= [finite-literal-B]) = ...

    • If [finite-literal-A] <= [finite-literal-B], then the result is (<= [finite-literal-B])

    • Else, the result is (<= [finite-literal-A])

    • e.g. (<= 3) | (<= 5.5) = (<= 5.5) because (<= 5.5) is a super-type of (<= 3)

  • (<= [finite-literal-A]) | (< [finite-literal-B]) = ...

    • If [finite-literal-A] == [finite-literal-B], then the result is (<= [finite-literal-A])

    • If [finite-literal-A] < [finite-literal-B], then the result is (< [finite-literal-B])

    • Else, the result is (<= [finite-literal-A])

  • (< [finite-literal-A]) | (< [finite-literal-B]) = ...

    • If [finite-literal-A] <= [finite-literal-B], then the result is (< [finite-literal-B])

    • Else, the result is (< [finite-literal-A])

    • e.g. (< 3) | (< 5.5) = (< 5.5) because (< 5.5) is a super-type of (< 3)

Also,

  • (<= A|B) = (<= A) | (<= B)
  • (< A|B) = (< A) | (< B)

    • e.g. (< 4|3) = (< 4) | (< 3) = (< 4)

    • e.g. (< number|3) = (< number) | (< 3) = number | (< 3) = number


Intersection GtEq/Gt types

When taking the intersection of two GtEq/Gt types, the type with "fewer" values is the result,

  • (>= [finite-literal-A]) & (>= [finite-literal-B]) = ...

    • If [finite-literal-A] >= [finite-literal-B], then the result is (>= [finite-literal-A])

    • Else, the result is (>= [finite-literal-B])

    • e.g. (>= 3) & (>= 5.5) = (>= 5.5) because (>= 5.5) is a sub-type of (>= 3)

  • (>= [finite-literal-A]) & (> [finite-literal-B]) = ...

    • If [finite-literal-A] == [finite-literal-B], then the result is (> [finite-literal-B])

    • If [finite-literal-A] > [finite-literal-B], then the result is (>= [finite-literal-A])

    • Else, the result is (> [finite-literal-B])

  • (> [finite-literal-A]) & (> [finite-literal-B]) = ...

    • If [finite-literal-A] >= [finite-literal-B], then the result is (> [finite-literal-A])

    • Else, the result is (> [finite-literal-B])

    • e.g. (> 3) & (> 5.5) = (> 5.5) because (> 5.5) is a sub-type of (> 3)

Intersection LtEq/Lt types

When taking the intersection of two LtEq/Lt types, the type with "fewer" values is the result,

  • (<= [finite-literal-A]) & (<= [finite-literal-B]) = ...

    • If [finite-literal-A] <= [finite-literal-B], then the result is (<= [finite-literal-A])

    • Else, the result is (<= [finite-literal-B])

    • e.g. (<= 3) & (<= 5.5) = (<= 3) because (<= 3) is a sub-type of (<= 5.5)

  • (<= [finite-literal-A]) & (< [finite-literal-B]) = ...

    • If [finite-literal-A] == [finite-literal-B], then the result is (< [finite-literal-B])

    • If [finite-literal-A] < [finite-literal-B], then the result is (<= [finite-literal-A])

    • Else, the result is (< [finite-literal-B])

  • (< [finite-literal-A]) & (< [finite-literal-B]) = ...

    • If [finite-literal-A] <= [finite-literal-B], then the result is (< [finite-literal-A])

    • Else, the result is (< [finite-literal-B])

    • e.g. (< 3) & (< 5.5) = (< 3) because (< 3) is a sub-type of (< 5.5)


Use Cases

  • To statically ensure an integer can fit in a MySQL UNSIGNED INT data type,

    //TODO Propose numeric and range sum/subtraction/multiplication/division/mod/exponentiation types?
    function insertToDb (x : int & (>= 0) & (<= 4294967295)) {
      //Insert to database
    }
    
  • To statically ensure a string is of a given length,

    function foo (s : string & { length : int & (>= 1) & (<= 255) }) {
      //Do something with this non-empty string that has up to 255 characters
    }
    
  • To statically ensure an array-like object has the appropriate length value,

    function foo (arr : { length : int & (>= 0) }) {
      //Do something with the array-like object
    }
    
  • To statically ensure we are only given finite numbers,

    function foo (x : double) {
      //`x` is NOT NaN|Infinity|-Infinity
    }
    
  • To statically ensure array indices exist?

    function foo (arr : { [index : int & (>= 0) & (< 10)] : string }) {
      console.log(arr[0]); //OK
      console.log(arr[1]); //OK
      console.log(arr[2]); //OK
      console.log(arr[9]); //OK
      console.log(arr[10]); //Error
    }
    

I would love to propose numeric and range addition/subtraction/multiplication/division/mod/exponentiation types but that seems out-of-scope with this issue.

[Edit]
You could rename double and call it float but I just thought that double more accurately represented that this is a double precision floating point number.

[Edit]

Changed some types to never.

Would it be possible to have compiler did the flow analysis?

Suppose there is this function

function DoSomething(x : int & (>= 0) & (< 10)){
   // DoSomething
}

function WillError(x : int){
    DoSomething(x); // error; x is not >= 0 & < 10
}

function WillNotError(x : int){
    if(x >= 0 && x < 10)
        DoSomething(x); // not error by flow analysis
}

one more use case: i have a number input to a function that represents a percentage. i want to limit values to being between 0 & 1.

I just ran [...Array(256)].map((_,i) => i).join("|") to make my ugliest type definition yet

Nonnegative integers and small numbers are possible:

type ArrayT<T> = T extends (infer P)[] ? P : never;
type A = ArrayT<Range<5, 10>>;//5|6|7|8|9|10

Range:https://github.com/kgtkr/typepark/blob/master/src/list.ts

By "small numbers" you mean without using stage 3 BigInt?

@Mouvedia A number that fits within the compiler's recursion limit

Maybe use .. for integers and ... for floats.

I would say that .. should mean inclusive range and ... exclusive. Just like in e.g. Ruby. See http://rubylearning.com/satishtalim/ruby_ranges.html

I'm not a fan of using a single period to differentiate between inclusive and exclusive ranges

Can I jump in? I think that this feature seen as a single enhancement for type specification is not likely the best way to go here.

type range = 1:2:Infinity // 1, 3, 5… Infinity

This is how you define a numeric range in many 4gl platforms — specifically matrix-oriented ones.

You do it this way, because a uniform range is conventionally one of the best modelling approaches.

So a sequential range is this:

type decmials = 1:10 // like 1 .. 10 with all decimals in between

And if just the integers:

type integers = 1:1:10 // 1, 2, 3, 4, 5, 6, 7, 8, 10

// OR

type integers = number.integers<1:10>

If we want to play around with syntax:

type something = 1::10 // whatever use cases need today

If that is not tokenizer friendly, this is not the right blocking priority to focus on here in my opinion. I point this out hoping to not see one solution limit us in getting to the next.

edit: What I fail to account for is the inclusiveness aspect — and here we must wonder if we are doing enough due diligence to understand why that was not an issue when people solved so many problems relying on uniform ranges that are implicitly inclusive of every number except when the increment did not exactly align with the end of the range.

Interesting that you bring up being able to define "step" sizes in a range type.

Beyond just "floating point" (no fixed step size) and "integer" (step size of 1, starts from an integer) ranges, I've never encountered a real life use case for ranges with other step sizes.

So, it's interesting to hear about it being a thing elsewhere. Now, I'll need to learn about 4gl because I've never heard of it before.


Being able to define a half-open interval is useful for array-like objects. Something like,

interface SaferArray<T, LengthT extends integer & (>= 0)> {
    length : LengthT;
    [index in integer & (>= 0) & (< LengthT)] : T
}

If we only had inclusive ranges, we'd need (<= LengthT - 1) but that's just less elegant

Can I jump in? I think that this feature seen as a single enhancement for type specification is not likely the best way to go here.

type range = 1:2:Infinity // 1, 3, 5… Infinity

This is how you define a numeric range in many 4gl platforms — specifically matrix-oriented ones.

You do it this way, because a uniform range is conventionally one of the best modelling approaches.

So a sequential range is this:

type decmials = 1:10 // like 1 .. 10 with all decimals in between

And if just the integers:

type integers = 1:1:10 // 1, 2, 3, 4, 5, 6, 7, 8, 10

// OR

type integers = number.integers<1:10>

If we want to play around with syntax:

type something = 1::10 // whatever use cases need today

If that is not tokenizer friendly, this is not the right blocking priority to focus on here in my opinion. I point this out hoping to not see one solution limit us in getting to the next.

edit: What I fail to account for is the inclusiveness aspect — and here we must wonder if we are doing enough due diligence to understand why that was not an issue when people solved so many problems relying on uniform ranges that are implicitly inclusive of every number except when the increment did not exactly align with the end of the range.

Hmm looks like the way it's handled in Haskell. I think this is good. Generators will allow for lazy evaluation as well.

It's not like you can't have lazy evaluation with any other syntax =x

Interesting that you bring up being able to define "step" sizes in a range type.

I think of a range as start, end and increment. So if the increment rounds off to exactly the end then it is inclusive of that. Array indices and lengths as range functions are can be an array 0:1:9 has 10 index steps (length). So the range here can conveniently be integer & 0:9 for a type system that can more easily infer from combining those two expressions.

4GL is really a generic label, for me that was mostly MatLab

My point was that having only inclusive ranges would make using it in generics harder (unless you implement the hack-y type-level math operations).

Because, instead of only having "length" as a type parameter, you now need length and max index. And max index must equal length-1.

And, again, you can't actually check that is the case unless you implement those hack-y type level math operations

@AnyhowStep I was thinking of the best way to frame your concern — but maybe it helps to first clarify:

If we put the inclusive-or-not as specifically applicable to any n-1 problem (like this index/count scenario) aside, assuming that was independently solved somehow (just humour the thought), are there other scenarios for which numeric ranges would still require less declarative and/or unconventional syntax to properly describe?

I am getting at those being 2 separate aspects, you need numeric ranges that are conventionally aligned with the expectations of that domain and you also need predicates for index/count types... etc.

Positive numbers only.

declare let x : (> 0);
x = 0.1; //OK
x = 0.0001; //OK
x = 0.00000001; //OK
x = 0; //Error
x = 1; //OK
x = -1; //Error

With inclusive-only ranges,

declare let x : epsilon:epsilon:Infinity; //Where epsilon is some super-small non-zero, positive number

Positive numbers, excluding Infinity,

declare let x : (> 0) & (< Infinity);

With inclusive-only ranges,

const MAX_FLOAT : 1.7976931348623157e+308 = 1.7976931348623157e+308;
declare let x : epsilon:epsilon:MAX_FLOAT;

Also, unrelated to the current discussion but here's another reason I really want numeric range types.

A lot of my day job is just cobbling a bunch of different software systems together. And a lot of these systems have different requirements for data that have the same meaning.

e.g, (systemA, value between 1 and 255), (systemB, value between 3 and 73), etc.
e.g. (systemC. string length 7-88), (systemD, string length 9-99), (systemE, string length 2-101), etc.

Right now, I need to carefully document all these separate requirements and make sure data from one system can correctly map to another system. If it doesn't map, I need to figure out workarounds.

I am only human. I make mistakes. I don't realize data can't map sometimes. Range checks fail.

With numeric range types, I can finally just declare the ranges each system expects and let the compiler do the checking for me.


For example, I just had a situation where the API I was using had a 10k string length limit for all string values. Well, I had no way to tell TypeScript to check that all strings going to the API are <= 10k string length.

I had a run-time error instead of a nice compile-error where TS could go,

`string` is not assignable to `string & { length : (<= 10000) }`

@AnyhowStep I hope you can appreciate that my intent is merely to make sure that if something called "Range as Number type" simply aligns with common expectations to the more conventional user (ie someone moving to TS wondering why the range emphasizes inclusivity over interval)

I honestly think that use cases should always drive features, and think that the issues relating to index and length are ones that many of us sometimes really miss. So I simply want to address that problem just under the correct label — is it a number type thing or an indexed type thing? I don't know the answer exactly, but I am hesitant to think that solving it as a number type thing will not inadvertently create far more problems for number type users not sharing the same understanding of these aspects.

So where else would fixing such a problem make sense to everyone is all I am wondering at this point, thoughts?

I'm dealing with an API that passes byte arrays, so I'd like to define a byte type:

type byte = 0x00..0xFF
type bytes = byte[]

This would also be useful when working with Uint8Array.

If you're like me and you're impatient about getting range types you can actually use at the moment, here's a code snippet of range types in action.

TL;DR,
Range types can work now

interface CompileError<_ErrorMessageT> {
    readonly __compileError : never;
}
///////////////////////////////////////////////
type PopFront<TupleT extends any[]> = (
    ((...tuple : TupleT) => void) extends ((head : any, ...tail : infer TailT) => void) ?
    TailT :
    never
);
type PushFront<TailT extends any[], FrontT> = (
    ((front : FrontT, ...tail : TailT) => void) extends ((...tuple : infer TupleT) => void) ?
    TupleT :
    never
);
type LeftPadImpl<TupleT extends any[], ElementT extends any, LengthT extends number> = {
    0 : TupleT,
    1 : LeftPad<PushFront<TupleT, ElementT>, ElementT, LengthT>
}[
    TupleT["length"] extends LengthT ?
    0 :
    1
];
type LeftPad<TupleT extends any[], ElementT extends any, LengthT extends number> = (
    LeftPadImpl<TupleT, ElementT, LengthT> extends infer X ?
    (
        X extends any[] ?
        X :
        never
    ) :
    never
);
type LongerTuple<A extends any[], B extends any[]> = (
    keyof A extends keyof B ?
    B :
    A
);

///////////////////////////////////////////////////////
type Digit = 0|1|2|3|4|5|6|7|8|9;
/**
 * A non-empty tuple of digits
 */
type NaturalNumber = Digit[];

/**
 * 6 - 1 = 5
 */
type SubOne<D extends Digit> = {
    0 : never,
    1 : 0,
    2 : 1,
    3 : 2,
    4 : 3,
    5 : 4,
    6 : 5,
    7 : 6,
    8 : 7,
    9 : 8,
}[D];

type LtDigit<A extends Digit, B extends Digit> = {
    0 : (
        B extends 0 ?
        false :
        true
    ),
    1 : false,
    2 : LtDigit<SubOne<A>, SubOne<B>>
}[
    A extends 0 ?
    0 :
    B extends 0 ?
    1 :
    2
];


//false
type ltDigit_0 = LtDigit<3, 3>;
//true
type ltDigit_1 = LtDigit<3, 4>;
//false
type ltDigit_2 = LtDigit<5, 2>;


/**
 * + Assumes `A` and `B` have the same length.
 * + Assumes `A` and `B` **ARE NOT** reversed.
 *   So, `A[0]` is actually the **FIRST** digit of the number.
 */
type LtEqNaturalNumberImpl<
    A extends NaturalNumber,
    B extends NaturalNumber
> = {
    0 : true,
    1 : (
        LtDigit<A[0], B[0]> extends true ?
        true :
        A[0] extends B[0] ?
        LtEqNaturalNumberImpl<
            PopFront<A>,
            PopFront<B>
        > :
        false
    ),
    2 : never
}[
    A["length"] extends 0 ?
    0 :
    number extends A["length"] ?
    2 :
    1
];
type LtEqNaturalNumber<
    A extends NaturalNumber,
    B extends NaturalNumber
> = (
    LtEqNaturalNumberImpl<
        LeftPad<A, 0, LongerTuple<A, B>["length"]>,
        LeftPad<B, 0, LongerTuple<A, B>["length"]>
    > extends infer X ?
    (
        X extends boolean ?
        X :
        never
    ) :
    never
);

//false
type ltEqNaturalNumber_0 = LtEqNaturalNumber<
    [1],
    [0]
>;
//true
type ltEqNaturalNumber_1 = LtEqNaturalNumber<
    [5,2,3],
    [4,8,9,2,3]
>;
//false
type ltEqNaturalNumber_2 = LtEqNaturalNumber<
    [4,8,9,2,3],
    [5,2,3]
>;
//true
type ltEqNaturalNumber_3 = LtEqNaturalNumber<
    [5,2,3],
    [5,2,3]
>;
//true
type ltEqNaturalNumber_4 = LtEqNaturalNumber<
    [5,2,2],
    [5,2,3]
>;
//false
type ltEqNaturalNumber_5 = LtEqNaturalNumber<
    [5,1],
    [2,5]
>;
//false
type ltEqNaturalNumber_6 = LtEqNaturalNumber<
    [2,5,7],
    [2,5,6]
>;

type RangeLt<N extends NaturalNumber> = (
    number &
    {
        readonly __rangeLt : N|undefined;
    }
);
type StringLengthLt<N extends NaturalNumber> = (
    string & { length : RangeLt<N> }
);

type AssertStringLengthLt<S extends StringLengthLt<NaturalNumber>, N extends NaturalNumber> = (
    LtEqNaturalNumber<
        Exclude<S["length"]["__rangeLt"], undefined>,
        N
    > extends true ?
    S :
    CompileError<[
        "Expected string of length less than",
        N,
        "received",
        Exclude<S["length"]["__rangeLt"], undefined>
    ]>
);
/**
 * String of length less than 256
 */
type StringLt256 = string & { length : RangeLt<[2,5,6]> };
/**
 * String of length less than 512
 */
type StringLt512 = string & { length : RangeLt<[5,1,2]> };

declare function foo<S extends StringLengthLt<NaturalNumber>> (
    s : AssertStringLengthLt<S, [2,5,6]>
) : void;

declare const str256 : StringLt256;
declare const str512 : StringLt512;

foo(str256); //OK!
foo(str512); //Error

declare function makeLengthRangeLtGuard<N extends NaturalNumber> (...n : N) : (
    (x : string) => x is StringLengthLt<N>
);

if (makeLengthRangeLtGuard(2,5,6)(str512)) {
    foo(str512); //OK!
}

declare const blah : string;
foo(blah); //Error

if (makeLengthRangeLtGuard(2,5,5)(blah)) {
    foo(blah); //OK!
}

if (makeLengthRangeLtGuard(2,5,6)(blah)) {
    foo(blah); //OK!
}

if (makeLengthRangeLtGuard(2,5,7)(blah)) {
    foo(blah); //Error
}

Playground

It uses the CompileError<> type from here,
https://github.com/microsoft/TypeScript/issues/23689#issuecomment-512114782

The AssertStringLengthLt<> type is where the magic happens

Using type-level addition, you can have, str512 + str512 and get a str1024

https://github.com/microsoft/TypeScript/issues/14833#issuecomment-513106939

Looking at @AnyhowStep solution I think I have better temporary solution. Remember type guards?:

/**
 * Just some interfaces
 */
interface Foo {
    foo: number;
    common: string;
}

interface Bar {
    bar: number;
    common: string;
}

/**
 * User Defined Type Guard!
 */
function isFoo(arg: any): arg is Foo {
    return arg.foo !== undefined;
}

/**
 * Sample usage of the User Defined Type Guard
 */
function doStuff(arg: Foo | Bar) {
    if (isFoo(arg)) {
        console.log(arg.foo); // OK
        console.log(arg.bar); // Error!
    }
    else {
        console.log(arg.foo); // Error!
        console.log(arg.bar); // OK
    }
}

doStuff({ foo: 123, common: '123' });
doStuff({ bar: 123, common: '123' });

So take a look at following code:

class NumberRange {
    readonly min: number;
    readonly max: number;
    constructor(min:number, max:number, ) {
        if (min > max) { 
            throw new RangeError(`min value (${min}) is greater than max value (${max})`);
        } else {
            this.min = min;
            this.max = max;
        }
    }
    public isInRange = (num: number, explicit = false): boolean => {
        let inRange: boolean = false;
        if (explicit === false) {
            inRange = num <= this.max && num >= this.min;
        } else {
            inRange = num < this.max && num > this.min;
        }
        return inRange;
    };
}
const testRange = new NumberRange(0, 12);
if(testRange.isInRange(13)){
    console.log('yay')
}else {
  console.log('nope')
}

It's just an idea but with type guards it's possible to use ranges.

The problem here is that you can't do this,

declare const a : number;
declare let b : (>= 5);
const testRange = new NumberRange(12, 20);
if (testRange.isInRange(a)) {
  b = a; //ok
} else {
  b = a; //compile error
}

Type guards alone are not a satisfactory solution

Also, your example doesn't even use type guards.


My silly hacky toy example above lets you assign a range type to another range type (through function params), if its bounds are inside the other.


Also, tag types, nominal types, and value objects being used on primitives is usually a sign that the type system isn't expressive enough.

I hate my silly toy example that uses tag types because it's extremely unergonomic. You can refer to this lengthy comment for how it's better for ranges to be a primitive.

https://github.com/microsoft/TypeScript/issues/6579#issuecomment-548249683

You may achieve this with current Typescript version:

// internal helper types
type IncrementLength<A extends Array<any>> = ((x: any, ...xs: A) => void) extends ((...a: infer X) => void) ? X : never;
type EnumerateRecursive<A extends Array<any>, N extends number> = A['length'] extends infer X ? (X | { 0: never, 1: EnumerateRecursive<IncrementLength<A>, N> }[X extends N ? 0 : 1]) : never;

// actual utility types
export type Enumerate<N extends number> = Exclude<EnumerateRecursive<[], N>, N>;
export type Range<FROM extends number, TO extends number> = Exclude<Enumerate<TO>, Enumerate<FROM>>;

// usage examples:
type E1 = Enumerate<3>; // hover E1: type E1 = 0 | 1 | 2
type E2 = Enumerate<10>;  // hover E2: type E2 = 0 | 1 | 3 | 2 | 4 | 5 | 6 | 7 | 8 | 9

type R1 = Range<0, 5>; // hover R1: type R1 = 0 | 1 | 3 | 2 | 4
type R2 = Range<5, 11>; // hover R2: type R2 = 10 | 5 | 6 | 7 | 8 | 9

I followed the convention of having inclusive start and exclusive end indexes, but You may adjust it to Your needs.

The vs code hover hints in comments.

Notice that the numbers in the hover hints are randomly sorted.

image

Sadly it only works up to around 10 elements :(

Edit: it seems that Enumerate can only handle up to 15 recursions (0 - 14)

@Shinigami92
With this approach, Enumerate handles up to 40.
This is still a limitation, but may be not so harsh in practice.

type PrependNextNum<A extends Array<unknown>> = A['length'] extends infer T ? ((t: T, ...a: A) => void) extends ((...x: infer X) => void) ? X : never : never;
type EnumerateInternal<A extends Array<unknown>, N extends number> = { 0: A, 1: EnumerateInternal<PrependNextNum<A>, N> }[N extends A['length'] ? 0 : 1];
export type Enumerate<N extends number> = EnumerateInternal<[], N> extends (infer E)[] ? E : never;
export type Range<FROM extends number, TO extends number> = Exclude<Enumerate<TO>, Enumerate<FROM>>;

type E1 = Enumerate<40>;
type E2 = Enumerate<10>;
type R1 = Range<0, 5>;
type R2 = Range<5, 34>;

And now it happened to be sorted somehow magically ;).

Typical use cases for me use ranges like [1, 255], [1, 2048], [1, 4096], [20, 80], etc. Creating large union types can make TS freak out/slow down. But those solutions definitely work for "smaller" ranges

@AnyhowStep
Knowing that recursion count is a limit, we should find the way to perform number division/multiplication by two in single non-recursive operation inside type definition to achieve these ranges.

Performance is still an issue. I've had to cull useful union types from large apps before for this reason - whilst it might be an interesting exercise, it's definitely not a solution.

There's still not pefect solutions I guess?

I hope this can be done as following in general way (but hard though).

type X => (number >= 50 && number > 60 || number = 70) || (string.startsWith("+"))

(i.e. normal javascript statement / function can be used here, except the variables are replaced by type)

In my understanding, modern language = normal logic + meta logic, where meta logic = code checking + code generation. A lot of works are just trying to merge meta logic syntax in an elegant way.

Maybe we can somehow assume range types not to be some kind of sugar, but to be core concept?

// number literal extend `number` despite the fact 
// that `number` is not union of all number literals
type NumberLiteralIsNumber = 5 extends number ? true : false // true

// so if we will define Range (with some non-existing syntax which is clearly should be done in lib internals)
type Range<MIN extends number, MAX extends number> = MAX > MIN ? 5 and 2NaN : MAX === MIN ? MAX : MIN..MAX

// and assume that `number` is Range<-Infinity, +Infinity>
type NumberIsInfinitRange = number extends Range<-Infinity, +Infinity> ?
    Range<-Infinity, +Infinity> extends number ? true : false :
    false // true

// following things will be true
type AnyRangeIsNumber<T extends number, K extends Number> = 
    Range<T, K> extends number ? true : false // true
type NestedRangeIsNumber<T extends number, K extends number, S extends number> =
    Range<T, Range<K, S>> extends number ? true : false // true

To complete this we need to declare behaviour on some cases

  1. Range<T, T> === T by definition
  2. Range<5, 1> === NaN by definition
  3. Range<NaN, T> === Range<T, NaN> === NaN as such value cannot exist
  4. Range<1 | 2, 5> === Range<2, 5> the posibility of larger number as first type parameter restricts the range to be shorter
  5. Range<1, 4 | 5> === Range<1, 4> the posibility of lower number as second type parameter restricts the range to be shorter
  6. 1.5 extends Range<1, 2> should be true by definition
  7. Range<Range<A, B>, C> === NaN extends Range<A, B> ? NaN : Range<B, C> follows from 5 and 3
  8. Range<A, Range<B, C>> === NaN extends Range<B, C> ? NaN : Range<A, B> follows from 6 and 3

And a little about Ranges inside Ranges:

type RangeIsInsideRange<T extends Range<any, any>, K extends Range<any, any>> = 
    T extends Range<infer A, infer B> 
        ? K extends Range<infer C, infer D> 
            ? NaN extends Range<A, C> 
                ? false 
                : NaN extends Range<B, D> 
                    ? false 
                    : true 
            : never
        : never

Range subtypes could even be defined in a generic way, as a type level comparison function (a: T, b: T) => '<' | '=' | '>'.

...is there a proposal for using regular JS functions at type level?

There's no proposal, per sey, but I have written a quick prototype before. There are large concerns over IDE performance and input sanitization, unfortunately.

@yudinns problem is Range exists at least at this name, so it may be confusing, see: https://developer.mozilla.org/en-US/docs/Web/API/range also I miss the functionality where I can define number series like <1, n+2> - step 2 starting from 1 or more complicated equation.

This TC39 proposal will probably reach stage 4 before this gets implemented.
The ticket is 3 years old.

Suggestion: Regex-validated string type: https://github.com/Microsoft/TypeScript/issues/6579

(related SO question: https://stackoverflow.com/questions/3895478/does-javascript-have-a-method-like-range-to-generate-a-range-within-the-supp)

It would also be cool if you could do something like number in range e.g. if 1 in 1..2 or inclusive if 1 in 1..=2 rust handles it well https://doc.rust-lang.org/reference/expressions/range-expr.html . That would save a lot of space

Don't python and rust already have a typed range feature?
Can we just reimplement existing solutions in other languages instead of reinventing the wheel or making it extra generic with strings, floats etc while the main purpose and biggest usecase are number ranges. We can add more later when needed

I have encountered this on more than one occasion and came here search for a way to define a type that is a fraction between 0...1 - this is being discussed on https://news.ycombinator.com/item?id=24362658#24372935 because there is no correct word in English to define these types of values. If typescript can help here that would be excellent but it may be extremely difficult to enforce at the type level.

@andrewphillipo I saw that being discussed as well, and learned about the term Unit Interval from one of the answers on stack exchange, which seems to be the correct way of referring to that specific range in a generic context.

If ranges as types are ever implemented in typescript, perhaps UnitInterval could be defined as a global type, to encourage some common nomenclature around something that we use frequently in programming.

Yes UnitInterval is the correct name for the range 0...1 so it's correct for the type! Still not correct for the naming of the number so it would be excellent if this was available to describe our code more precisely using such a type - how does this work under the hood in Rust's type system - is it just a guard or?

If CantorSpace isn't too much of a stretch, I think that would be a fair definition of the "range" of "all" real numbers between [0, 1] as understood by a computer. Assigning these to values at compile time can't be inferred by a lower and upper bound of javascript's Math.floor or Math.ceil since Math.ceil(0) === 0, and Math.floor(1) === 1 which is unfortunate.

If the set of all numbers (0, 1) were used instead it'd work using the example above, but that's kind of bad to exclude values referenced as percentages in everyday language. If possible, having the compiler include the boundaries somehow would be nice, maybe through a strict === check against the edge cases 0 and 1.

or Maybe use 1~~3 for integers and 0.1~0.5 for floats ?

~ is already taken by the unary bitwise NOT operator.

Why we need to talk about those damn floats, the problem we have is with whole numbers. 99.99% is integer problem 0.001% is float problem.
No general solution easily possible so let's go for the number range of 0..10 as seen in most programming languages, as already implemented years ago.

Stop the float talking

Maybe we can split the bill and exand this proposal for the the int case:
https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c

And for floats, I'm currently working on a different design proposal based on some ideas in this issue (primarily @AnyhowStep; basically the ability to have an Open-Interval-Type).

Just keep it simple with
x..y for whole numbers only.
0.1..2 would give an error
1..2.1 would also give an error
And any detectable non whole number.
Heck copy the implementation logic from
https://en.m.wikibooks.org/wiki/Ada_Programming/Types/range
Or
https://kotlinlang.org/docs/reference/ranges.html
Or
https://doc.rust-lang.org/reference/expressions/range-expr.html
and call it a day.

✅No need to reinvent the wheel
✅No need to account for floats
✅Consistency across language
✅Tested and copy able stable code from existing sources

It's worth the effort to solve floats, since "int" based type checking is trivial for numbers < 1000 by hard coding the type value as a union of every fixed integer 0..1000, so saying that there's "no need" to "re-invent the wheel" is a little ironic. Why not use what works already for simple use cases?

I would rather see for some room to allow for a type definition which understands things like irrational numbers. The ability to detect invariants at compile time when working with radians would be an interesting use case. Supporting τ as an argument to a range value boundary is a good target to claim that this feature works as intended imo.

Keep in mind that there is no explicit integer in javascript. Everything is just a number, which is stored as a float. If the range type operator is implemented, it should handle any value that number can handle.

@aMoniker depending on the definition of explicit: BigInt

Even for bigint, there are use cases unions can't solve like making sure a bigint fits within a MySQL signed/unsigned bigint

What about creating an intrinsic range type for this feature?

lib.es5.d.ts (or lib.es2020.bigint.d.ts for including support for bigints)
type GreaterThan<N extends number | bigint> = intrinsic
type GreaterThanOrEqualTo<N extends number | bigint> = GreaterThan<N> | N
type LessThan<N extends number | bigint> = intrinsic
type LessThanOrEqualTo<N extends number | bigint> = LessThan<N> | N

/**
 * prevent `GreaterThan` and `LessThan` from desugaring
 * (in the same way that `number` does _not_ desugar to `-Infinity | ... | Infinity`)
 */
userland
type GreaterThanOrEqualTo2 = GreaterThanOrEqualTo<2>
type LessThan8 = LessThan<8>

type GreaterThanOrEqualTo2_And_LessThan8 = GreaterThanOrEqualTo2 & LessThan8
type LessThan2_Or_GreaterThanOrEqualTo8 = LessThan<2> | GreaterThanOrEqualTo<8> // inverse of `GreaterThanOrEqualTo2_And_LessThan8` (would be nice to be able to just do `Exclude<number | bigint, GreaterThanOrEqualTo2_And_LessThan8>` but that might be wishful thinking)
type LessThan2_And_GreaterThanOrEqualTo8 = LessThan<2> & GreaterThanOrEqualTo<8> // `never`
type GreaterThanOrEqualTo2_Or_LessThan8 = GreaterThanOrEqualTo2 | LessThan8 // `number | bigint`

type RangesAreNumbersOrBigIntsByDefault = LessThan8 extends number | bigint ? true : false // `true` (the user could always narrow this on a per-range basis, e.g. `LessThan8 & number`)
type RangesAcceptUnions = LessThan<7n | 7.5 | 8> // `LessThan<8>`
type RangesAcceptOtherRanges1 = LessThan<LessThan8> // `LessThan8`
type RangesAcceptOtherRanges2 = LessThan<GreaterThanOrEqualTo2> // `number | bigint`
type RangesSupportBeingInAUnion = (-6 | 0.42 | 2n | 2 | 3) | LessThan<2> // `LessThan<2> | 2n | 2 | 3`
type RangesSupportBeingInAnIntersection = (-6 | 0.42 | 2n | 2 | 3) & LessThan<2> // `-6 | 0.42`
type RangesSupportBeingInAUnionWithOtherRanges = LessThan<2> | LessThan8 // `LessThan8`
type RangesSupportBeingInAnIntersectionWithOtherRanges = LessThan<2> & LessThan8 // `LessThan<2>`

@RyanCavanaugh

This really needs some compelling use cases, because the implementation of unions today is completely unsuited to realizing unions this large. Something that would happen if you wrote something like this ...

The following will be unsound without an int type:

const arr: string[] = ['a', 'b', 'c']
const item = arr[1.01]  // Typescript inferred this as string but actually it is undefined
console.log(item)  // Will print undefined, we miss inferred the type

Having the follow type will prevent this problem:

type TUInt = 0..4294967295;

Another use case for having Int32 and Int64 type specifically is when people start to annotation their code with it ... will open the door to better interoperability with other languages ... almost all statically typed languages have an integer type: Java, C#, C, Rust, F#, Go ... etc.

If I want to call a npm library written in TypeScript from C# for example, there are libraries that take TypeScript definitions and create an interface for me in C# but the problem is the number type is float which can't be used to index an array in C# without casing it ... etc.

Other use cases: easier to transpile between langauges, performance optimization ... etc

Note that one more case where this would be useful is interaction with WebAssembly, which has very explicit separate number types (I guess it was briefly mentioned as C# use-case, but wanted to clarify that it applies much wider than that).

UPD: Nvm, I see it was mentioned in https://github.com/microsoft/TypeScript/issues/15480#issuecomment-365420315 which Github has "helpfully" hid as "hidden items".

+1

Was this page helpful?
0 / 5 - 0 ratings

Related issues

uber5001 picture uber5001  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments

Antony-Jones picture Antony-Jones  ·  3Comments

siddjain picture siddjain  ·  3Comments

dlaberge picture dlaberge  ·  3Comments