Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active June 20, 2024 05:26
Show Gist options
  • Save rbuckton/5fd81582fdf86a34b45bae82d842304c to your computer and use it in GitHub Desktop.
Save rbuckton/5fd81582fdf86a34b45bae82d842304c to your computer and use it in GitHub Desktop.
Proposal: Range Types for TypeScript

Range Types (T[X:Y]) for TypeScript

A Range Type is a type that produces a new tuple or array type that derives from T by dropping the excess ordered elements of T outside of the range starting at offset X (inclusive) through offset Y (exclusive).

A Range Type is denoted using a : operator in what would otherwise be an Indexed Access Type:

type T = [1, 2, 3][0:2];

NOTE: The Range Type is a type-space only addition and has no impact on value-space syntax as it cannot be used in a JavaScript expression.

Motivations

  • Support for slicing a number of elements from anywhere within a Tuple Type.
    • Current approaches for this depend on the use of Conditional Types, Infer Types, and recursion hacks using Indexed Access Types and Type Literals. This means they have limited use as they quickly run into recursion limits and have almost no support for type relationships.
  • Improve expression level support when using Array.prototype.slice on a Tuple Type.
  • Support converting an Array Type into a Tuple Type when the size of the Tuple Type is known.
  • With other future proposals, is intended to further improve type checking for Function.prototype.bind and other high-order "pipe" or "chain"-like behaviors.

Syntactic Rules

The : operator is parsed at the same precedence level as an Indexed Access Type. A Range Type is comprised of an object type, an optional start type, and an optional end type:

RangeType: Type `[` Type? `:` Type? `]`

Semantic Rules

  • The result of a Range Type has the same mutability as its object type.
  • The object type of a Range Type is constrained to be an Array Type or a Tuple Type.
  • The start type and end type of a Range Type are constrained to string | number.
  • The start type of a Range Type is optional. If not present, the lower bound type (0) is used.
  • The end type of a Range Type is optional. If not present, the upper bound type (^0) is used (see Inverse Offset Type).
  • If the start type or end type of a Range Type are negative-valued Numeric Literal Types (or numeric index-valued String Literal Types), they are treated as an Inverse Offset Type for the absolute value of the numeric index value of the respective type.
    • NOTE: This does not work for -0 as JavaScript generally treats -0 and 0 as the same value except for a few small corner cases and we must align with this behavior.
  • If the object type is a "generic object type", or if either of the start type or end type are "generic index types", then the operation is deferred until it they can be instantiated.
  • If either the object type, start type, or end type are Union Types, the result is distributed over each constituent in the following manner:
    • The object type is distributed over any Inverse Offset Type constituent of start type or end type. This is necessary as an Inverse Offset Type can have a different outcome depending on the object type it is resolved against.
    • The start type and end type are distributed over each constituent of the object type.
    • The object type is distributed over each constituent of the start type and end type.
    • The results of the distribution are either an Intersection Type (if the Range Type was a "write" location), or a Union Type (if the Range Type was a "read" location).
  • Otherwise (or for each constituent of the distribution),
    • If neither the start type nor the end type are String Literal Types or a Numeric Literal Types, then
      • Return an Array Type for the union of each element of the object type: T[any:any] -> T[number][].
    • If the start type is neither a String Literal Type nor a Numeric Literal Type, then
      • Return an Array Type for the union of each element of the Range Type for the same object type and end type, but with a start type of 0: T[any:Y] -> T[:Y][number][].
    • If the end type is neither a String Literal Type nor a Numeric Literal Type, then
      • Return an Array Type for the union of each element of the Range Type for the same object type and start type, but with an end type of ^0: T[X:any] -> T[X:][number][].
    • If the start type is the lower bound type (0) and the end type is the upper bound type (^0), then we are including all elements: return the object type of the Range Type.
    • If the start type is the upper bound type (^0) or the end type is the lower bound type (0), then we are including no elements: return the empty Tuple Type ([]).
    • If the object type is an Array Type, then
      • If the signs of both the start type and end type agree, then return a fixed-length Tuple Type with a minimum length of 0 for the difference between the end type and the start type whose elements are the element type of the object type: T[][0:1] -> [T?].
      • If the start type is an Inverse Offset Type and the end type is not, we can create a tuple of min(abs(start), end) optional elements.
      • Otherwise, we cannot derive a fixed length: return the object type of the Range Type.
    • Otherwise, the object type is a Tuple Type:
      • If the object type has a rest element, then
        • If the start type is an Inverse Offset Type, return an Array Type for the union of each optional type and the rest element type, along with the set of n right-most required elements of the object type where n is the absolute value of the start type: [A, B, ...C[]][^1:] -> (B | C)[].
        • If the end type is an Inverse Offset Type, return a Tuple Type of the elements of the object type starting from the index at start type, but whose minimum length is reduced by the absolute value of the end type: [A, B, C, ...D[]][1:^1] -> [B, C?, ...D[]]
      • Otherwise,
        • Clamp the start type and end type to values between 0 and the length of object type.
        • Return a Tuple Type for the elements of object type starting at start type and ending at end type.

Assignability

  • S is assignable to T[X:Y] if S is assignable to C, where C is the base constraint of T[X:Y] for writing.
  • S[X:Y] is assignable to T[] if S[number] is assignable to T.
  • S[XS:YS] is assignable to T[XT:YT] if S is assignable to T, XS is assignable to XT, and YS is assignable to YT.

Examples

[1, 2, 3][:];          // -> [1, 2, 3][0:3]        -> [1, 2, 3]
[1, 2, 3][:1];         // -> [1, 2, 3][0:1]        -> [1]
[1, 2, 3][1:];         // -> [1, 2, 3][1:3]        -> [2, 3]
[1, 2, 3][1:1];        // -> []
[1, 2, 3][^1:];        // -> [1, 2, 3][2:3]        -> [3]
[1, 2, 3][:^1];        // -> [1, 2, 3][0:2]        -> [1, 2]
[1, ...2[]][:2];       // -> [1, 2?]
[1, 2, 3, ...4[]][^1:] // -> (3 | 4)[]
[1, 2, 3][number:^1]   // -> [1, 2, 3][number:2]   -> [1, 2, 3][:2][number][] -> (1 | 2)[]
[1, 2, 3][never:]      // -> (1 | 2 | 3)[]
[1, 2, 3][:never]      // -> (1 | 2 | 3)[]
[1, 2, 3][any:]        // -> (1 | 2 | 3)[]
[1, 2, 3][any:2]       // -> (1 | 2)[]
[1, 2, 3][:any]        // -> (1 | 2 | 3)[]
[1, 2, 3][1:any]       // -> (2 | 3)[]
[1, 2, 3][number:]     // -> (1 | 2 | 3)[]
[1, 2, 3][number:2]    // -> (1 | 2)[]
[1, 2, 3][:number]     // -> (1 | 2 | 3)[]
[1, 2, 3][1:number]    // -> (2 | 3)[]
[1, 2, 3][:1 | 2]      // -> [1, 2?]
[1, 2, 3]["0":^"0"]    // -> [1, 2, 3]
[1, 2, 3]["a":"b"]     // -> never
number[][:];           // -> number[]
number[][1:];          // -> number[]
number[][:1];          // -> [number?]

Prior Art

Related

@KevinGhadyani-Okta
Copy link

A lot of number passing is done at runtime which is probably why this hasn't been added to TypeScript.

Even still, if your runtime APIs are also type-safe, you can easily ensure the numbers coming in have to be sanitized by the consumer (user of a function), rather than the producer (function itself) because TypeScript won't allow passing an invalid number.

There's also a lot of compute cost required in generating range types that are unions of ~1000 numbers. Having those built-in would also improve performance substantially.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment