Repository navigation
Proposal: Interval Types / Inequality Types #43505
Description
Activity
nikeee commented
on Apr 2, 2021 ContributorAuthorMore actionsI wanted to get into the TS compiler, so I've implemented a simple prototype of this type system extension. You can play around with it here:
https://nikeee.github.io/typescript-intervalsThese features are not implement / have bugs:
- Discriminated unions (see above for reason why)
- Enum support
- Generics
elsebranch sometimes does not receive the correct flow type
Reacted by whzx5byb, thewavelength, Bradd Carey, Neale Upstone, Moritz Mahringer, Kyle a.k.a. TechSquidTV, Julian Avar, bstro, Hadrian de Oliveira, 愚安 and 10 moreReacted by Ricardo Fernández Serrata- addedIn DiscussionNot yet reached consensusNot yet reached consensusSuggestionAn idea for TypeScriptAn idea for TypeScript
on Apr 2, 2021 Some other things to explicitly disregard as goals, but may help steer design and discussion:
- positive vs. negative positional vectors
- periodic vs. aperiodic series
- convergent vs. divergent series
These features, if they existed, would allow for compile-time detection of changes to a value in a "direction" that is not supported. For instance, a model of a closed system with some variable representing a range of values for
Entropy. Not only is it(>=0) & (<=1), it must always trend in one direction only.From there, a periodic revolution allows for a "reversing" of the positional vector, but only if they exceed the threshold of the upper limit, and immediately returns to the lower limit. Support for this is not obvious to me, and would be difficult.
The third point is similar in that it's not obvious, but in my opinion would be even more difficult to do correctly. Given a positional vector and a limit, how can brief violations of this constraint be tolerated in order to facilitate convergent series for
transcendentalcomputed values, e.g., the approximation ofφfrom the Fibonacci series?Reacted by RA80533 and shimevailyI'm not sure I can just do this, but I'd like to suggest the
%type wherex extends (% y)(assuming both x and y are number literals) iffx % y == 0. This would effectively maketype integer = (% 1)possible, as well. Interaction with<,>etc should probably be left untouched except maybe(> 0) & (< 5) & (% 3)should become3, and(> 0) & (< 2) & (% 3)becomesneverMight've also been proposed already, but I couldn't find anything
Reacted by Andrew Yurisich, Jan Buschtöns, peterlits zo and LEE HYEONUSo did it ever get implemeneted?
Regarding to the roadmap, no.
nikeee commented
on Apr 22, 2021 ContributorAuthorMore actionsIn its current state, it's just a proposal that is subject for discussion. So feel free to post some questions, use-cases and enhancements! :)
This would be nifty! I ran into a use case while building a Markdown parser, that shows some interaction with template literal types as well. Here's a very simplified version:
type HeadingLevel = 1|2|3|4|5|6; function makeHeading(level: HeadingLevel): HTMLHeadingElement { // already correctly inferred as a HTMLHeadingElement return document.createElement(`h${level}` as const); } function getHeadingLevel(text: string): HeadingLevel|null { let match = /(#{1,6}) (\w+)/.exec(text); if(match === null) { return null; } let pounds: string = match[1]; // would be nice if this were inferred to be (>=0) let level = pounds.length; // interval types could help to avoid this cast! if(level > 0 && level <= 6) { return level as HeadingLevel; } return null; } let n = getHeadingLevel("#### heading"); if(n !== null) { makeHeading(n); }
Here's my use case. I want to define a union type for an API result, something like this:
type ApiResult<T> = | { total_results: 0; } | { total_results: 1; data: T; } | { total_results: number; // integer >= 2 data: T[]; };so that I can later narrow the type by checking the value of
total_results, e.g.:let apiResult: ApiResult<string>; if (apiResult.total_results > 1) { // apiResult.data is string[] } else if (apiResult.total_results === 1) { // apiResult.data is string } else { // apiResult.data is not present }Reacted by Niklas Mollenhauer and Valery Zinchenkonikeee commented
on Jun 18, 2021 ContributorAuthorMore actionsMaybe another wording could be something like "Inequality Types" originating from inequalities
- changed the title
[-]Proposal: Interval Types[/-][+]Proposal: Interval Types / Inequality Types[/+]on Jun 18, 2021 is it inferable in extends clause ?
type MinRange<I extends number> = I extends (>= infer T) ? T : never; type T0 = MinRange<(>= 100)>; // T0 will be 100
Reacted by Sebastian Hädrichnikeee commented
on Jul 15, 2021 ContributorAuthorMore actionsThat should be possible aswell (when generic support is implemented, the playground does not support that).
can it works well with template type ?
type Hex = (>=0) & (<= 255) type HexStr = `0x${Hex}`
12 remaining items
One thing I'd like to request is that conditional types be improved with regards to
numberliterals,numberintervals andnumbers.e.g.
type Test1 = 5 extends NumberInterval<0, 10> ? true : false; // -> true type Test2 = NumberInterval<0, 10> extends number ? true : false; // -> true type Test3 = number extends NumberInterval<-Infinity, Infinity> ? true : false; // -> false type Test4 = (typeof NaN) extends number ? true : false; // true type Test5 = (typeof NaN) extends NumberInterval<-Infinity, Infinity> ? true : false; // false
The benefit would be that we could discern between a number literal and a number when writing a conditional type and treat them differently.
Imo creating a generic type is better than simply throwing a
>=between parenthesis. Like this answer which supposedly only works for small numbers.nikeee commented
on Nov 1, 2022 ContributorAuthorMore actionsYeah, maybe an intrinsic implementation for generic types like
GreaterThan<T extends number | bigint>would be more DX-friendly. That also wouldn't require an extension to the grammar.I support the proposal. It can be implemented via intrinsic types or via a new syntax. This playground shows how the intrinsic generic types could behave by outputting the constrained types as strings for manual evaluation.
Instead of limiting the syntax to(> 0)I propose(number > 0). I am not sure about the full syntax implications, but it might look something like this in real world:type Fill = { value: string repeat: number > 0 };
It would make this feature more open for extension, like putting any union of number literals or constrained number types on the left of the operator while the union on the right side is a bit meaningless, but still possible and still yields the expected result if the intrinsic generic types are distributive.
type PositiveNumbers = GreaterThan<number, 0>;// `(${number} > 0)` type NegativeNumbers = LessThan<number, 0>; // `(${number} < 0)` type Probability = GreaterThanOrEqual<number, 0> & LessThanOrEqual<number, 1>; // resolves to `(${number} >= 0)` & `(${number} <= 1)` // Subject for discussion: Handling of (>1|2|3) type SubjectForDiscussion = GreaterThan<number, 1 | 2 | 3>; // resolves to `(${number} > 1)` | `(${number} > 2)` | `(${number} > 3)` // would be reduced to `(${number} > 1)` // (>10) | (>20) type Reduction1 = GreaterThan<number, 10> | GreaterThan<number, 20>; // resolves to `(${number} > 10)` | `(${number} > 20)` // would be reduced to `(${number} > 10)` // The same, but rely on the distributive property // (>10) | (>20) type Reduction1_ = GreaterThan<number, 10 | 20> // distributes to `(${number} > 10)` | `(${number} > 20)` // would be reduced to `(${number} > 10)` // (>10) & (>20) type Reduction2 = GreaterThan<number, 10> & GreaterThan<number, 20>; // resolves to `(${number} > 10)` & `(${number} > 20)` // would be reduced to `(${number} > 20)` // The same, but rely on the distributive property // (>10) & (>20) type Reduction2_ = GreaterThan<number, 10 | 20>; // distributes to `(${number} > 10)` & `(${number} > 20)` // would be reduced to `(${number} > 20)` // // Now apply the same operation on subsets of number // type Stars = 0 | 1 | 2 | 3 | 4 | 5; type PositiveReviews = GreaterThan<Stars, 2>; // distributes to // "(0 > 2)" | "(1 > 2)" | "(2 > 2)" | "(3 > 2)" | "(4 > 2)" | "(5 > 2)" // "(never)" | "(never)" | "(never)" | "(3)" | "(4)" | "(5) " // // 3 | 4 | 5 type PositiveButNotExcellentReviews = LessThan<PositiveReviews, 5>; // distributes to // "((0 > 2) < 5)" | "((1 > 2) < 5)" | "((2 > 2) < 5)" | "((3 > 2) < 5)" | "((4 > 2) < 5)" | "((5 > 2) < 5)" // "((never) < 5)" | "((never) < 5)" | "((never) < 5)" | "(3 < 5) " | "(4 < 5) " | "(4 < 5) " // "(never) " | "(never) " | "(never) " | "(3) " | "(4) " | "(never) " // // 3 | 4
Reacted by Luca Schultz, Henry Ruhs, Caleb Russel Elat, Hubert and Valery ZinchenkoReacted by Caleb Russel ElatIf you were just wanting to detect whether a value was a
numberor a numeric literal (e.g.4), then this will works:type IsNumericLiteral<T> = number extends T ? false : T extends number ? true : false; type T = IsNumericLiteral<5>; // true type U = IsNumericLiteral<number>; // false
You might also want to detect whether you are creating a union of types, too, which can be done like so:
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ( k: infer I ) => void ? I : never; type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true; type Not<A> = A extends true ? false : true; type IsNotUnion<T> = Not<IsUnion<T>>; type T = IsNotUnion<5>; // true type U = IsNotUnion<5 | 3>; // false
Reacted by Wang Zishi and Karol MajewskiAs a temporary solution, you can use the following type:
type NumericRange< start extends number, end extends number, arr extends unknown[] = [], acc extends number = never, > = arr['length'] extends end ? acc | start | end : NumericRange<start, end, [...arr, 1], arr[start] extends undefined ? acc : acc | arr['length']>; type MyRange = NumericRange<1, 50>;
Reacted by Alexander Gusarov, Mahesh Bansod, Spencer Evans, id3er0, Neil Pankey, Aashish Dhakal, kennarddh, Karol Majewski, dmytro-mysak, Burton and 1 moreI support this proposal too.
It's a much needed feature when
noUncheckedIndexedAccesstsconfig compiler option is turned on. Arrays constantly need undefined checks otherwise, like so:const a = [1,2,3] if(a[1] === undefined) { throw new Error('undefined') } const b: number = a[10] // ts can infer that there's been an undefined check, so the type of b can only be number nowIt would be nice to have an method of guaranteeing the length and that the elements are therefore defined. Something like this type:
type Len<T extends readonly any[], L extends number> = Readonly<Pick<Required<T>, 0..L>>The
0..Lis where I need an interval or range type. I can use a union at the moment, but this is a very finite solution (I don't want to be typing out a union with 10's of thousands of entries) and inflexible. I can use a recursive type to build the union type automatically, but these fall down after ~3000 entries on my machine (due to stack overflow of recursion).Reacted by Arseniy Terekhin, Maru Alka, Trevin Miller and YounesMlikAs a temporary solution, you can use the following type:
type NumericRange< start extends number, end extends number, arr extends unknown[] = [], acc extends number = never, > = arr['length'] extends end ? acc | start | end : NumericRange<start, end, [...arr, 1], arr[start] extends undefined ? acc : acc | arr['length']>; type MyRange = NumericRange<1, 50>;
Very nice !! That's exactly what I need, but there is a problem with negative values :/ :
- if start = -a and end > 0, there is no values in the generated type for the interval ]-a; 0].
- if start = -a and end = -b with -a < -b, there is an error
I will see if it can be fixed.
My use-case is simple - User roles. The names are ambiguated
export enum UserRole { Guest = -1, CompanyManager, CompanyOwner, SuperManager, SuperOwner } export interface GeneralUser { id: string email: string firstName: string lastName: string avatar: string } export interface SuperUser extends GeneralUser { role: UserRole.SuperOwner | UserRole.SuperManager companyId?: string signed: true } export interface CompanyUser extends GeneralUser { role: UserRole.CompanyOwner | UserRole.CompanyManager companyId: string signed: true } export interface GuestUser extends GeneralUser { role: UserRole.Guest signed: false } export type User = SuperUser | CompanyUser | GuestUser
And currently I'm receiving error when I try to use
Usertype by narrowing it with<=comparison (it only works with===):if (user.role === UserRole.Guest) return if (user.role <= UserRole.CompanyOwner) { // `CompanyUser` cast is used since TS doesn't handle `<=` properly. MyScript.set("COMPANY_ID", { companyId: (user as CompanyUser).companyId }) }
So I'm putting the comment above to signalize it, I will also reference this issue, so maybe someone will refactor it in future 😘
BTW, I really like this proposal, I think it will be very handy.
Valery Zinchenko (@FrameMuse)
I suppose your user variable is typed by User.
So inside the if block, user type is CompanyUser | GuestUser, but companyId doesn't exist for the GuestUser type, so this is normal to have a type error.
You can see this error by adding this line inside the if block: const companyId: string = user.companyId;
You have the error :
Property 'companyId' does not exist on type 'User'. Property 'companyId' does not exist on type 'GuestUser'.The problem is not the Typescript parser, but your code. And this is wrong to cast user as CompanyUser, because it could be a GuestUser for which the companyId property doesn't exist.
Regards.
Reacted by Valery Zinchenko and Ricardo Fernández SerrataValery Zinchenko (@FrameMuse) I suppose your user variable is typed by User. So inside the if block, user type is CompanyUser | GuestUser, but companyId doesn't exist for the GuestUser type, so this is normal to have a type error
I also noticed that in the code so I introduced another
if, but it's not narrowing properly anyway. Thanks for the notice.Valery Zinchenko (@FrameMuse) I suppose your user variable is typed by User. So inside the if block, user type is CompanyUser | GuestUser, but companyId doesn't exist for the GuestUser type, so this is normal to have a type error
I also noticed that in the code so I introduced another
if, but it's not narrowing properly anyway. Thanks for the notice.Indeed, it is strange...
If you replace your if expression by this one:
user.role !== UserRole.Guest && user.role <= UserRole.CompanyOwner)user is now seen as SuperUser | CompanyUser inside the if block, which is wrong because SuperUser should have been excluded by the second criterium of the if expression. =/
But you should not report this bug here, because this issue is about to add new types definition operators, not the types resolution implementation. Watch if an existing issue talks about your bug, otherwise create a new one.
Regards.
Reacted by Ricardo Fernández SerrataBut you should not report this bug here, because this issue is about to add new types definition operators, not the types resolution implementation. Watch if an existing issue talks about your bug, otherwise create a new one.
I couldn't find one and now I'm too lazy to create a new one, so I leave it here... Thanks anyway
Reacted by Ricardo Fernández Serrata and Maru AlkaReacted by Ricardo Fernández SerrataReacted by vincent-guyon_Atos, Niccolò Maltoni, Ricardo Fernández Serrata and Maru AlkaBut you should not report this bug here, because this issue is about to add new types definition operators, not the types resolution implementation. Watch if an existing issue talks about your bug, otherwise create a new one.
I couldn't find one and now I'm too lazy to create a new one, so I leave it here... Thanks anyway
You can be sure it will be ignored here... -_-°...
Reacted by Ricardo Fernández Serrata and Guy CarmeliReacted by Ricardo Fernández Serrata and Guy CarmeliReacted by Valery Zinchenko and Guy Carmelisamal-rasmussen commented
on Jan 28, 2026 More actionsAs a temporary solution, you can use the following type:
type NumericRange<
start extends number,
end extends number,
arr extends unknown[] = [],
acc extends number = never,= arr['length'] extends end
? acc | start | end
: NumericRange<start, end, [...arr, 1], arr[start] extends undefined ? acc : acc | arr['length']>;type MyRange = NumericRange<1, 50>;
Very nice !! That's exactly what I need, but there is a problem with negative values :/ :
* if start = -a and end > 0, there is no values in the generated type for the interval ]-a; 0]. * if start = -a and end = -b with -a < -b, there is an errorI will see if it can be fixed.
Also it only goes up to 999. Error on 1000.
Reacted by vincent-guyon_AtosReacted by Maru Alka
Related Proposals:
This proposal is partly based on AnyhowStep's comment and describes the addition of an
IntervalTypeto TypeScript.📃 Goals / Motivation
Provide developers with a type system that prevents them from forgetting range checks on numbers similar to how we prevent them from forgetting string validation and numeric constant checks.
An interval primitive defines a range of numbers, limited up to a specific value. For example,
less than 10is an interval boundary (this wording may be subject to change).Non-Goals
Definitions
The idea is to extend type-narrowing for relational comparisons (
<,>,<=,>=), in addition to the currently existing mechanism to narrow by equality (==!=,===,!==).The emerging type is one that lays between literal types (a type with a single value) and the
numbertype.Syntax
So, in code we'd do something like this:
Side-Note: There is also a suggestion by @btoo to use the new
intrinsictype and generics instead of a distinct syntax (likeGreaterThan<5>).Semantics
Currently,
IntervalTypeLimitcan only ba aNumberLiteral. Allowing references to other types would make this feature significantly more different. There may be a useful opportunity when it comes to generics (see below).Assignability
A variable of type
Ais assignable to one of typeB:(> y)(>= y)(< y)(<= y)(> x)falsefalse(>= x)falsefalse(< x)falsefalse(<= x)falsefalseAssignability when constants are involved:
(> x)(>= x)(< x)(<= x)kNaNfalsefalsefalsefalseInfinitytruetruefalsefalse-InfinityfalsefalsetruetruenullfalsefalsefalsefalseundefinedfalsefalsefalsefalsefalsefalsefalsefalseSince the
IntervalTypeisNumberLike, one can do everything with it what can be done withnumber.Control-Flow
The core idea is that we extend type narrowing and combine that with union and intersection types:
Union and Intersection Types
Interval types can be used in an intersection type:
A union or intersection type may only contain at most two interval boundaries:
(>10) | (>20)will be reduced to(>10)(>10) & (>20)will be reduced to(>20)(<10) & (>10)will be reduced tonever(<10) | (>10)will not get simplified, it remains(<10) | (>10)(>=1) | (<1), see belowOther cases how interval boundaries interact with existing types:
number | (>1)is reduced tonumbernumber & (>1)is reduced to(>1)(>1) & <any non-number type>is reduced toneverSubject for discussion: Handling of
(>1|2|3)It may be appropriate to expand this as
(>1) | (>2) | (>3)(which would be normalized to(>1)). Or we just prohibit this kind of use.The case of
(>=1) | (<1)Consider this code:
In the else branch, we widen the type back to
numberinstead of narrowing to(<=9). This is because we'd also branch toelse, ifawould beNaN. So,numberimplicitly containsNaN.If a variable's type is/contains an interval boundary, its value cannot be
NaN.This opens up the question on how we should handle
(>=1) | (<1). Semantically, it is equivalent tonumber \ {NaN}, sonumberwould not be equivalent here.It would feel more natural to the developer if
(>=1) | (<1)would becomenumber(includingNaN), since not reducing it tonumberwould look weird.If we'd have negated types (#29317) as well as a
NaNtype, we could model this asnumber & not NaN. This may outweigh practical use and would be against the design rules (see the third entry in "Non-Goals"; this is an opinion).We also don't have a
NaNtype and there is currently no intent to introduce it.If we'd decide against the simplification to
number, it would make discriminated unions work more easily.Discriminated Unions via Interval Boundary Types?
Having discriminated unions based on intervals is not yet evaluated that much. Considering the problem with
(>=1) | (<1), this may not be possible. For example:Discriminated unions would work if the
elsebranch of the example in(>=1) | (<1)would resolve to(<=9). For that to work, we need to dropNaN.We could also solve this problem by not simplifying
(>=1) | (<1)tonumber, soResult["success"]would be(>0.5) | (<=0.5)instead ofnumber.Normalization of Unions and Intersections
When
ninterval boundaries are unified or intersected, the result will always be a single boundary, exactly two boundaries, a number literal,numberornever.Normalization is commutative, so one half of the table is empty.
Normalizing
A | B:(> y)(>= y)(< y)(<= y)(> x)(> min(x, y))(>= y):(> x)number:(> x) or (< y)(>= x) or (<= y):number(>= x)(>= min(x, y))number:(>= x) or (< y)(>= x) or (<= y):number(< x)(< max(x, y))(< x):(<= y)(<= x)(<= max(x, y))(due to a limitation of markdown tables, we use
orinstead of|)Normalizing
A & B:(> y)(>= y)(< y)(<= y)(> x)(> max(x, y))(> x):(>= y)(> x) & (< y):never(> x) & (<= y):never(>= x)(>= max(x, y))(>= x) & (< y):nevernever: (y == x ?y:(>= x) & (<= y))(< x)(< min(x, y))(<= y):(< x)(<= x)(<= min(x, y))If an interval is equality-compared to a literal, we either narrow down to the respective literal or
never.Further narrowing a boundary intersection via control flow will not increase the number of interval boundaries present in the type:
Assertions
a as (>1)(whereais anumberor another interval), just like with number literal types.Widening and Arithmetics
Similar to literal types, type-level arithmetics are not supported and coerce to
number. For example:Also, applying operators like
++and--let the type widen tonumber:However, due to the way interval types are always reduced to a maximum of two interval boundaries, it may be feasable to do type-level arithmetics. This proposal currently does not intent to do this.
Loops
With intervals, we can be more exact about loop variables. For example, narrowing a simple c-style for loop would come "for free" if we have flow-based type-narrowing that works for if-then-else:
Explanation on what happens:
With a little more work, it may be possible to let
ibe(>=initial-value) & (<10). But due to the type coercion that++causes, this is not possible out-of-the-box.Enum Interaction
It may be an interesting addition to allow enum literals as limit value, so we can type something like this:
It should also be possible to pass interval types to functions that take enums (just like numbers).
Generics
Generics may be valuable to support in this proposal, when they resolve to a literal type.
Consider this implementation of clamp:
💻 Use-Cases
We could statically type/document some APIs more explicitly, for example:
Math.randomMath.absclampNumber.rangeMath.sin/cos/...a % bifais assignable to(>=0)andbresolves to a number literal (falling back tonumberotherwise)Functions could express their assumptions about parameters.
They would be useful to force the developer to check if they are in range before passing (similar to literal types):
Feel free to add more use-cases!
✅ Viability Checklist
🔍 Search Terms / Keywords