Typescript: feature: ability to extract union of valid index numbers for tuple

Created on 15 Aug 2019  路  9Comments  路  Source: microsoft/TypeScript

Search Terms

indexof array indexof tuple restrict argument to tuple index

Suggestion

I'd like the ability to restrict a type to a valid index number of a tuple. For example, given the tuple [string], I'd like to extract the type 0. Given the tuple [string, number, object], I'd like to extract the type 0 | 1 | 2.

Use Cases

_see related S.O. question_

Currently, if a generic class receives a type argument which is a tuple, it isn't possible to create a method on the class which restricts its argument to a valid index of that tuple.

Examples

class FormArray<T extends [any, ...any[]]> {
  constructor(public value: T) {}

  // how to restrict `I` to only valid index numbers of `T` ?
  get<I extends keyof T>(index: I): T[I] {
    return this.value[index];
  }
}

Implementation ideas

I imagine there are several routes to achieving my goal.

Idea 1 (perhaps heavy handed):

Add a new keyword, such as indexof which can only be used on arrays and tuples. If indexof is used on an array, it will always return number. If indexof is used on a tuple, it returns a union of 0 .... tuple length - 1 (e.g. if the tuple was of length 3 indexof would return 0 | 1 | 2).

Idea 2:

Ability to cast a string type to the equivalent number type. For example, the ability to cast type "0" to 0. This would allow using the keyof keyword to get all properties of a tuple, cast property types to numbers (if possible), and then filter out any property types which aren't numbers (i.e. filter out "length", "splice", etc. and leave 0 | 1 | 2).

For example, as pointed out in this comment, it is currently possible to get the indexes of a tuple in string form (i.e. "0" | "1" | "2").

type ArrayKeys = keyof any[];
type Indices<T> = Exclude<keyof T, ArrayKeys>;

Indices<[string, number]>; // "0" | "1"

Idea 3:

As pointed out in a comment, you can get the index numbers of a tuple using the following type:

type Indices<T extends {length: number}> = Exclude<Partial<T>["length"], T['length']>;

Unfortunately, the result of this type is not considered a keyof the input tuple (which results in a type error if you try and use the type as a key for the tuple). If there were some way of using a type assertion to tell the compiler that this is, in fact, a keyof T, that might also work.

_note: this type differs from the type presented in idea 2 (above) because, unlike this type, the type in idea 2 is a keyof T_

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
Awaiting More Feedback Suggestion

Most helpful comment

@AlCalzone Do you need Drop1? Can you not just exclude the length of the original tuple?

All 9 comments

For the time being, you can see a workaround in the S.O. issue I liked to above

type ArrayKeys = keyof any[];

type StringIndices<T> = Exclude<keyof T, ArrayKeys>;

interface IndexMap {
  "0": 0,
  "1": 1,
  "2": 2,
  "3": 3,
  "4": 4,
  "5": 5,
  "6": 6,
  "7": 7,
  "8": 8,
  "9": 9,
  "10": 10,
  "11": 11,
  "12": 12,
}
type CastToNumber<T> = T extends keyof IndexMap ? IndexMap[T] : number;

type Indices<T> = CastToNumber<StringIndices<T>>;

class FormArray<T extends [any, ...any[]]> {
  constructor(public value: T) {}

  get<I extends Indices<T>>(index: I): T[I] {
    return this.value[index];
  }
}

EDIT: Disregard all this voodoo, easier solution below...

I'm not on my computer but what about
Partial<T>["length"]?

I think it should work. Can't test it at the moment, though

Partial\["length"]

Damn! Never thought of this... :eyes: But that includes one union item too much.

Partial<Drop1<T>>["length"] works with

type Drop1<T extends any[]> = ((...args: T) => void) extends ((a1: any, ...rest: infer R) => void) ? R : never;

At least provide the Drop1<> type =P
You deleted all your code

@AlCalzone Do you need Drop1? Can you not just exclude the length of the original tuple?

So having just tested, I find type Indices<T extends {length: number}> = Exclude<Partial<T>["length"], T['length']>; does work pretty well. Unfortunately, given type T = any[] and type I = Indices<T>, I is not considered a keyof T which is kindof the whole point.

So this errors (playground link):

type Indices<T extends {length: number}> = Exclude<Partial<T>["length"], T['length']>;

class FormArray<T extends [any, ...any[]]> {
  constructor(public value: T) {}

  // Here, `T[I]` errors because `I` is not a `keyof T`
  get<I extends Indices<T>>(index: I): T[I] {
    return this.value[index];
  }
}

In at least my case, @dragomirtitian's solution (which I repurposed above) is probably the best solution for the time being.

This all being said, I don't understand why Partial<T>["length"] works? Why isn't Partial<T>["length"] equal to T['length'] | undefined? Does it just come down to how the type of Array#length is internally calculated by typescript?

@thefliik Partial<[number, number, number]> is the same as [number?, number?, number?] (all items in the tuple are optional). So this means any tuple of length between 0 and 3 is assignable to this tuple. This means that the length can be 0 | 1 | 2 | 3.

Ashamed I did not think of it 馃槥

Interesting. I guess I've never used an optional property in a tuple. Optionals are so often equivalent to undefined, I would have expected [number?, number?, number?] to be equivalent to [number | undefined, number | undefined, number | undefined] and always be of length 3.

The reality is more useful. Still unless there is some way of getting the compiler to realize that Indices<T> is keyof T, this solution has limited usefulness. Edit: I suppose you could use as any or @ts-ignore to force Indices<T> to be accepted, but that's obviously non-ideal.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bgrieder picture bgrieder  路  3Comments

weswigham picture weswigham  路  3Comments

wmaurer picture wmaurer  路  3Comments

uber5001 picture uber5001  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments