Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Range as Number type #15480

Closed
streamich opened this issue Apr 30, 2017 · 151 comments
Closed

Suggestion: Range as Number type #15480

streamich opened this issue Apr 30, 2017 · 151 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@streamich
Copy link

streamich commented Apr 30, 2017

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
}

type RandomDice = 1..6;

const roll: RandomDice = Math.floor(Math.random() * 6);
// Error: -------------------------^ Maybe use Math.ceil()?
@panuhorsmalahti
Copy link

panuhorsmalahti commented Aug 18, 2017

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

@goodmind
Copy link

goodmind commented Aug 20, 2017

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.

@streamich
Copy link
Author

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

@aluanhaddad
Copy link
Contributor

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

@streamich
Copy link
Author

@aluanhaddad Say probability:

type TProbability = 0.0...1.0;

@aluanhaddad
Copy link
Contributor

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

@jcready
Copy link

jcready commented Aug 20, 2017

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

@fatcerberus
Copy link

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.

@aluanhaddad
Copy link
Contributor

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

@streamich
Copy link
Author

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;

@RyanCavanaugh RyanCavanaugh added the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Aug 22, 2017
@RyanCavanaugh
Copy link
Member

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

@jcready
Copy link

jcready commented Aug 22, 2017

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.

@weswigham
Copy link
Member

@RyanCavanaugh Subtraction types? 🌞

@streamich
Copy link
Author

streamich commented Aug 23, 2017

Negative types, type negation.

Anything but a string:

type NotAString = !string;

Any number except zero:

type NonZeroNumber = number & !0;

@kitsonk
Copy link
Contributor

kitsonk commented Aug 23, 2017

@streamich subtraction types are covered by #4183

@RoyTinker
Copy link

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

@aluanhaddad
Copy link
Contributor

@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]);
  }
}

@Frikki
Copy link

Frikki commented Dec 4, 2017

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

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Dec 5, 2017

@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;

@streamich
Copy link
Author

@aluanhaddad But no unsigned int:

type UInt = 0..4294967295;

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Dec 5, 2017

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

@streamich
Copy link
Author

streamich commented Dec 5, 2017

Using * operator from @Aleksey-Bykov comment:

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

@gcnew
Copy link
Contributor

gcnew commented Dec 6, 2017

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

@RoyTinker
Copy link

RoyTinker commented Dec 6, 2017

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

@tresabhi
Copy link

tresabhi commented Jul 22, 2022

> With this approach, Enumerate handles up to 40. This is still a limitation, but maybe not so harsh in practice.
@moccaplusplus Any way to not exclude the end TO index?

I figured it out just in like two minutes of posting it. I split it up into an exclusive and inclusive enumerator.

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 EnumerateInternalExclusive<A extends Array<unknown>, N extends number> = {
  0: A;
  1: EnumerateInternalExclusive<PrependNextNum<A>, N>;
}[N extends A['length'] ? 0 : 1];

type EnumerateInternalInclusive<A extends Array<unknown>, N extends number> = {
  0: PrependNextNum<A>;
  1: EnumerateInternalInclusive<PrependNextNum<A>, N>;
}[N extends A['length'] ? 0 : 1];

export type EnumerateExclusive<N extends number> = EnumerateInternalExclusive<
  [],
  N
> extends (infer E)[]
  ? E
  : never;

export type EnumerateInclusive<N extends number> = EnumerateInternalInclusive<
  [],
  N
> extends (infer E)[]
  ? E
  : never;

export type Range<FROM extends number, TO extends number> = Exclude<
  EnumerateInclusive<TO>,
  EnumerateExclusive<FROM>
>;

type E1 = EnumerateExclusive<93>;
type E2 = EnumerateExclusive<10>;
type R1 = Range<0, 5>;
type R2 = Range<5, 34>;

@jerrygreen
Copy link

jerrygreen commented Jul 22, 2022

Well, since it seems that nowadays Typescript types are Turing complete (#14833), there's nothing impossible in typing system of Typescript.

On the other hand, I wouldn't really like to add some 40 lines of too complex absolute weirdo code, into my project, for such a simple thing as «number range». So to me it's not an issue of formal possibility but native convenience.

P.S. One can create some AI or something someday, just using types.

@tresabhi
Copy link

That's news to me. I bet someone is currently trying to make Tetris out of type definitions right now.

@luke-robertson
Copy link

This would be good

@vasyop
Copy link

vasyop commented Aug 4, 2022

> With this approach, Enumerate handles up to 40. This is still a limitation, but maybe not so harsh in practice. @moccaplusplus Any way to not exclude the end TO index?

I figured it out just in like two minutes of posting it. I split it up into an exclusive and inclusive enumerator.

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 EnumerateInternalExclusive<A extends Array<unknown>, N extends number> = {
  0: A;
  1: EnumerateInternalExclusive<PrependNextNum<A>, N>;
}[N extends A['length'] ? 0 : 1];

type EnumerateInternalInclusive<A extends Array<unknown>, N extends number> = {
  0: PrependNextNum<A>;
  1: EnumerateInternalInclusive<PrependNextNum<A>, N>;
}[N extends A['length'] ? 0 : 1];

export type EnumerateExclusive<N extends number> = EnumerateInternalExclusive<
  [],
  N
> extends (infer E)[]
  ? E
  : never;

export type EnumerateInclusive<N extends number> = EnumerateInternalInclusive<
  [],
  N
> extends (infer E)[]
  ? E
  : never;

export type Range<FROM extends number, TO extends number> = Exclude<
  EnumerateInclusive<TO>,
  EnumerateExclusive<FROM>
>;

type E1 = EnumerateExclusive<93>;
type E2 = EnumerateExclusive<10>;
type R1 = Range<0, 5>;
type R2 = Range<5, 34>;

this is cool, but it needs too much memory even for pretty small ranges
also, the A['length'] trick doesn't work for negative numbers
we need actual syntax support for this feature

unknown

@scorbiclife
Copy link

FWIW, here's a 9 SLOC implementation for non-negative integers.
Hope it helps anyone.

// Increment / Decrement from:
// https://stackoverflow.com/questions/54243431/typescript-increment-number-type

// We intersect with `number` because various use-cases want `number` subtypes,
// including this own source code!

type ArrayOfLength<
  N extends number,
  A extends any[] = []
> = A["length"] extends N ? A : ArrayOfLength<N, [...A, any]>;
type Inc<N extends number> = number & [...ArrayOfLength<N>, any]["length"];
type Dec<N extends number> = number &
  (ArrayOfLength<N> extends [...infer A, any] ? A["length"] : -1);

type Range<Start extends number, End extends number> = number &
  (Start extends End ? never : Start | Range<Inc<Start>, End>);

@seahindeniz
Copy link

seahindeniz commented Nov 24, 2022

What about the issue with incrementing by 2 or even 10 instead of 1?
input

type Foo = Range<0, 100, 10>;
type Bar = Range< -100, 0, 10>;

equivalent

type Foo = 0 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100;
type Bar = -100 | -90 | -80 | -70 | -60 | -50 | -40 | -30 | -20 | -10 | 0;

@Thaina
Copy link

Thaina commented Nov 25, 2022

As of now we have more and more complex proposal that seem impossible to handle with just range. I think maybe we should considering generic constraint with pattern matching clause so we could write arbitrary guard logic

We could bring the whole pattern matching system from C# into typescript (include switch expression) and then allow using pattern matching at the type constraint

function DoSomething<T>(T value) : T extends number and >= 0 and <= 100 and % 10 === 0
{
    // value is 0 10 20 30 ... 100
}


DoSomething(60);
DoSomething(39); // error
let value = GetValue();
if(value extends number and >= 0 and <= 100 and % 10 === 0)
    DoSomething (value); // not error because same check

@minecrawler
Copy link

minecrawler commented Nov 25, 2022

@Thaina I like your idea, since it allows for complex ranges. However, how would you make sure that two constraints are compatible? How would you guide on useless ranges? For example:

let num1: number extends >= 0 and <= 100 and % 10 == 0 = 0;
let num2: number extends >= -1 and <= 100 and % 10 == 0 = 0;
let num3: number extends > 0 and < 3 and % 10 == 0; // assign what???

function foo(param0: number extends >= 0 and < 1337) {
  // do sth.
}

foo(num1); // OK, since the constraints are compatible
foo(0.1); // OK, since it matches the constraints
foo(num2); // ERROR!

Also, how about doing this a little more type-y:

let num1: number extends >= 0 & <= 100 & % 10 == 0 = 0;
let num2: number extends (>= -1 & <= 100) | (>= 1000 & < 10000) = 0;

@Thaina
Copy link

Thaina commented Nov 25, 2022

I am fine with any tweak of syntax as long as it can reproduce the same pattern as C# could

But useless range is responsibility of the code's author. It was the exact same as writing useless if. They might just got error when they try to assign anything to that variable

@Thaina
Copy link

Thaina commented Nov 25, 2022

There is also #50325

@bradzacher
Copy link
Contributor

One potential compelling argument is allowing additional validation of conditions.

For example imagine if the indexOf function on strings/arrays was defined as returning something like -1..n. Then it would be possible to validate that the following conditions are invalid:

  • indexOf() >= -1 - "error - expression is always true"
  • indexOf() < -1 - "error - expression is always false"
  • indexOf() === -2 - "error - expression is always false"

It's a small win for sure, but it's great for documentation and code correctness.

We were talking about this in @typescript-eslint (typescript-eslint/typescript-eslint#6126), but our issue is that without such a type-system representation we would essentially have to hard-code a list of functions and their expected return types within a lint rule. If there was a type-system representation then instead we could build a rule for the general case (if TS didn't include such checks).

@icecream17
Copy link

icecream17 commented Nov 30, 2022

Idea: By default, just store max and min (and step in some proposals).
If the range is too big, then say Exclude<Range<1, 100000>>, 7> can just be Range<1, 100000>. (At least initially)
This would solve the performance issues.

I think most of these use cases just want to use Range to check if a number is in the range (i.e. min <= num <= max, again accounting for step in some proposals) or some variant of that. Excluding a number from a large range is relatively rare.

Edit: This is probably the same as #43505

Also 1000 likes wow!

@Flaviano-Rodrigues
Copy link

This feature already work ?

@minecrawler
Copy link

@Flaviano-Rodrigues No. This is a topic about how it should work and what we need to consider in order to get a solid implementation.

@Flaviano-Rodrigues
Copy link

Oh, ok. I'm really excited to use this feature. case added

@captain-yossarian
Copy link

Hi,you can check my article and or stackoverflow answer .

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
> =
    (Result['length'] extends N
        ? Result
        : ComputeRange<N, [...Result, Result['length']]>
    )

type Add<A extends number, B extends number> = [...ComputeRange<A>, ...ComputeRange<B>]['length']

type IsGreater<A extends number, B extends number> = IsLiteralNumber<[...ComputeRange<B>][Last<[...ComputeRange<A>]>]> extends true ? false : true

type Last<T extends any[]> = T extends [...infer _, infer Last] ? Last extends number ? Last : never : never

type RemoveLast<T extends any[]> = T extends [...infer Rest, infer _] ? Rest : never

type IsLiteralNumber<N> = N extends number ? number extends N ? false : true : false


type AddIteration<
    Min extends number,
    Max extends number,
    ScaleBy extends number,
    Result extends Array<unknown> = [Min]
> =
    IsGreater<Last<Result>, Max> extends true
    ? RemoveLast<Result>
    : AddIteration<
        Min, Max, ScaleBy, [...Result, Add<Last<Result>, ScaleBy>]
    >

// [5, 13, 21, 29, 37]
type Result = AddIteration<5, 40, 8>

@171h
Copy link

171h commented Apr 22, 2023

Hi,you can check my article and or stackoverflow answer .

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
> =
    (Result['length'] extends N
        ? Result
        : ComputeRange<N, [...Result, Result['length']]>
    )

type Add<A extends number, B extends number> = [...ComputeRange<A>, ...ComputeRange<B>]['length']

type IsGreater<A extends number, B extends number> = IsLiteralNumber<[...ComputeRange<B>][Last<[...ComputeRange<A>]>]> extends true ? false : true

type Last<T extends any[]> = T extends [...infer _, infer Last] ? Last extends number ? Last : never : never

type RemoveLast<T extends any[]> = T extends [...infer Rest, infer _] ? Rest : never

type IsLiteralNumber<N> = N extends number ? number extends N ? false : true : false


type AddIteration<
    Min extends number,
    Max extends number,
    ScaleBy extends number,
    Result extends Array<unknown> = [Min]
> =
    IsGreater<Last<Result>, Max> extends true
    ? RemoveLast<Result>
    : AddIteration<
        Min, Max, ScaleBy, [...Result, Add<Last<Result>, ScaleBy>]
    >

// [5, 13, 21, 29, 37]
type Result = AddIteration<5, 40, 8>

// Error Type instantiation is excessively deep and possibly infinite.ts(2589)
type WorkMinute = AddIteration<0, 1439, 1>

@newghost
Copy link

Maybe we can implement by string type, for example we want to define types Hour/Minute or Time:

type zeroToNine = 0|1|2|3|4|5|6|7|8|9
type zeroToTwo = 0|1|2|''
type zeroToSix = 0|1|2|3|4|5|6|''

export type Hour = `${zeroToTwo}${zeroToNine}`
export type Minute = `${zeroToSix}${zeroToNine}`

export type Time =  `${Hour}:${Minute}`

Usage

export const startArchive = (_time: Time = '02:30') => {

}

@RyanCavanaugh
Copy link
Member

Picking up a clean slate at #54925

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Jul 7, 2023
@SaadBazaz
Copy link

6 years in! Would love to see this.

@shaedrich
Copy link

As of now we have more and more complex proposal that seem impossible to handle with just range. I think maybe we should considering generic constraint with pattern matching clause so we could write arbitrary guard logic

We could bring the whole pattern matching system from C# into typescript (include switch expression) and then allow using pattern matching at the type constraint

function DoSomething<T>(T value) : T extends number and >= 0 and <= 100 and % 10 === 0
{
    // value is 0 10 20 30 ... 100
}


DoSomething(60);
DoSomething(39); // error
let value = GetValue();
if(value extends number and >= 0 and <= 100 and % 10 === 0)
    DoSomething (value); // not error because same check

Or use patterns as RegExp like in #41160

@microsoft microsoft locked as resolved and limited conversation to collaborators Oct 23, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests