-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
This idea can be expanded to characters, e.g. |
I think this can be expanded and use syntax like and semantics like Haskell ranges.
|
@panuhorsmalahti What if you specify |
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. |
|
@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. |
@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 |
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 |
@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. 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 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:
|
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>; |
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. |
That's news to me. I bet someone is currently trying to make Tetris out of type definitions right now. |
This would be good |
this is cool, but it needs too much memory even for pretty small ranges |
FWIW, here's a 9 SLOC implementation for non-negative integers. // 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>); |
What about the issue with incrementing by 2 or even 10 instead of 1? 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; |
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 |
@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; |
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 |
There is also #50325 |
One potential compelling argument is allowing additional validation of conditions. For example imagine if the
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). |
Idea: By default, just store max and min (and step in some proposals). 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! |
This feature already work ? |
@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. |
Oh, ok. I'm really excited to use this feature. case added |
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) |
Maybe we can implement by string type, for example we want to define types Hour/Minute or Time:
Usage
|
Picking up a clean slate at #54925 |
6 years in! Would love to see this. |
Or use patterns as RegExp like in #41160 |
When defining a type one can specify multiple numbers separated by
|
.Allow to specify number types as ranges, instead of listing each number:
Maybe use
..
for integers and...
for floats.The text was updated successfully, but these errors were encountered: