Proposal: Interval Types #43505
Proposal: Interval Types #43505
Comments
|
I 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: These features are not implement / have bugs:
|
|
Some other things to explicitly disregard as goals, but may help steer design and discussion:
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 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 which approximate some transcendental value, such as |
Related Proposals:
This proposal is partly based on @AnyhowStep's comment and describes the addition of an
IntervalTypeto TypeScript.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 (#4196) 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.Coercion 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 coerce 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:
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!
The text was updated successfully, but these errors were encountered: