Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
5 tasks done
jorroll opened this issue Aug 15, 2019 · 10 comments
Open
5 tasks done

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

jorroll opened this issue Aug 15, 2019 · 10 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@jorroll
Copy link

jorroll commented Aug 15, 2019

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:

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

jorroll commented Aug 15, 2019

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];
  }
}

@AlCalzone
Copy link
Contributor

AlCalzone commented Aug 15, 2019

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

@AnyhowStep
Copy link
Contributor

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

@AlCalzone
Copy link
Contributor

AlCalzone commented Aug 15, 2019

Partial<T>["length"]

Damn! Never thought of this... 👀 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;

@AnyhowStep
Copy link
Contributor

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

@jack-williams
Copy link
Collaborator

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

@jorroll
Copy link
Author

jorroll commented Aug 15, 2019

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?

@dragomirtitian
Copy link
Contributor

@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 😞

@jorroll
Copy link
Author

jorroll commented Aug 15, 2019

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.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Aug 15, 2019
@perigean
Copy link

I think the Partial made the length field itself optional, which then introduces an | undefined to Indices. Intersecting number seems to help. This made the example from @jorroll work.

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

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

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

playground link.

I'm still quite a TS newbie though, so my reasoning might not be based on how it actually workd...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants