Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active February 11, 2020 20:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbuckton/53e335ce3d63686e229bc4ae25017756 to your computer and use it in GitHub Desktop.
Save rbuckton/53e335ce3d63686e229bc4ae25017756 to your computer and use it in GitHub Desktop.
Proposal: Inverse Offset Types for TypeScript

Proposal: Inverse Offset Types (^N) for TypeScript

This proposal adds an Inverse Offset Type to the TypeScript type-space. An Inverse Offset Type is a type notation used to indicate that Type should be considered as an offset from the end of an Array Type or Tuple Type.

An Inverse Offset Type is denoted using a "hat" operator (^) preceding a type:

type T = ^0;

NOTE: The Inverse Offset 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 "negative" indexes in Indexed Access Types against Array Types and Tuple Types, since -1 already has a valid meaning in an Indexed Access Type.
  • A mechanism for representing the exclusive upper bound of an Array Type or Tuple Type (^0), since the numeric literal type -0 is equivalent to 0.
  • A mechanism for negating a generic type.

Syntactic Rules

The ^ operator is parsed at the same precedence level as TypeOperator and InferType:

// NOTE: parens below indicate precedence
^A          -> (^(A))
^A | B      -> (^(A)) | (B)
keyof ^A    -> (keyof (^(A)))
^A[]        -> error: A[] does not satisfy the constraint ...
(^A)[]      -> ((^(A))[])

Semantic Rules

  • An Inverse Offset Type is deferred until applied as the index type of an Indexed Access Type or Range Type.
  • An Inverse Offset Type is deferred if its index type is generic.
  • An Inverse Offset Type is considered a "generic index type" if its index type is a "generic index type".
  • An Inverse Offset Type's index type is constrained to string | number.
    • We allow string for the purpose of coercing numeric index strings to their number equivalent for use with mapped types and keyof (i.e., "1" is equiavalent to 1).
  • The Inverse Offset Type of an Inverse Offset Type I is the index type of I: ^^I -> I
  • An Inverse Offset Type for negative index type is instead a Literal Type for the absolute value of the index type: ^-1 -> 1
  • If the index type of an Inverse Offset Type is a union, the Inverse Offset Type is distributed over the union: ^(A | B) -> ^A | ^B
  • When applied as the index type of an Indexed Access Type or the start type or end type of a Range Type:
    • If the object type of the Indexed Access Type or Range Type is a Union Type, the Inverse Offset Type is distributed over the object type:
      (T | U)[^N]       -> T[^N] | U[^N] (reading)
      (T | U)[^N]       -> T[^N] & U[^N] (writing)
      
      This is necessary as an Inverse Offset Type can have a different outcome depending on the object type it is resolved against.
    • If the outer object type of a containing Indexed Access Type is an Array Type, the Inverse Offset Type is essentially number: [1, 2][^N] -> (1 | 2)
    • If the outer object type of a containing Indexed Access Type is a Tuple Type with optional elements or a rest argument, the object type is distributed over the indices representing the rest element type, all optional types, and n fixed element types of the tuple starting from the right-most fixed element where n is the numerical index value of index type of the Inverse Offset Type:
      [1, 2, 3?, ...4[]][^1]  -> [1, 2][^1] | [1, 2, 3 | undefined][^1] | [1, 2, 3 | undefined, 4][^1]
                              -> 2          | (3 | undefined)           | 4
                              -> 2 | 3 | 4 | undefined
      
    • The index type is bounded by the upper and lower bounds of the Indexed Access Type or Range Type (i.e., for T[^N], N is clamped to the range 0 <= N <= T["length"]).
      • If the outer object type is an Array Type, or is a Tuple Type with a rest argument, there is no upper bound.

Assignability

  • ^S is assignable to string | number.
  • ^S is assignable to ^T if S is assignable to T.

Examples

// Indexed Access Types
type A = [1, 2, 3];
type B = A[^0]; // undefined
type C = A[^1]; // 3

// Range Types
type A = [1, 2, 3];
type B = A[0:^0];   // [1, 2, 3]
type C = A[0:^1];   // [1, 2]
type D = A[^1:^0];  // [3]

Prior Art

Related

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