I have a generic class where the type parameter is a tuple. I'm having trouble creating a method on the class that has an argument restricted to an index of the tuple.
For example (playground link):
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];
}
}
What I know you can do, is use keyof
to get all of the properties on the tuple, which will include the keys associated with the objects the tuple contains (i.e. "0", "1", etc). Unfortunately, keyof
pulls in all properties on the tuple, including "length", "splice", etc.
I tried using keyof
and excluding all properties which weren't of type number
, but then I realized that the index properties ("0", "1", etc) are returned by keyof
as type string
.
Is it possible to accomplish this currently in TypeScript?
You can exclude keyof any[]
from keyof T
and be left with only the appropriate tuple keys, they will be in string form unfortunately:
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
get<I extends Exclude<keyof T, keyof any[]>>(index: I): T[I] {
return this.value[index];
}
}
new FormArray([1,2, 3]).get("0");
Play
You can also add a mapping to number, but it will have to be a manual affair I'm afraid:
interface IndexMap {
0: "0"
1: "1"
2: "2"
3: "3"
4: "4"
/// add up to a resonable number
}
type NumberArrayKeys<T extends PropertyKey> = {
[P in keyof IndexMap]: IndexMap[P] extends T ? P : never
}[keyof IndexMap]
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) { }
// how to restrict I to only valid index numbers of T ?
get<I extends Exclude<keyof T, keyof any[]> | NumberArrayKeys<keyof T>>(index: I): T[I] {
return this.value[index];
}
}
let a = new FormArray([1, "2", 3]).get("0");
let b = new FormArray([1, "2", 3]).get("1");
let c = new FormArray([1, 2, 3]).get(0); // number
let d = new FormArray([1, "2", 3]).get(1); // string
Play
Note I am surprised T[I]
works even if I
is a number
even though keyof T
returns the indexes as string
not number
This realization lead me to another possible solution, where I
can also be a number
. If the number is in the tuple length range it will return the appropriate type, other wise it will return undefined
. It will not be an error on the invocation, but since the return value will be typed as undefined
if you use strictNullChecks
there is pretty much nothing you can do with it:
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) { }
// how to restrict I to only valid index numbers of T ?
get<I extends Exclude<keyof T, keyof any[]> | number>(index: I): T[I] {
return this.value[index];
}
}
let a = new FormArray([1, "2", 3]).get("0");
let b = new FormArray([1, "2", 3]).get("1");
let c = new FormArray([1, 2, 3]).get(0); // number
let d = new FormArray([1, "2", 3]).get(1); // string
let e = new FormArray([1, "2", 3]).get(10); // undefined
Play
You can obtain a union of numeric indices for a given tuple type using a conditional constrained inferred type within a template literal type like this:
type Indices<T extends any[]> =
Exclude<keyof T, keyof any[]> extends `${infer I extends number}`
? I
: never;
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
get<I extends Indices<T>>(index: I): T[I] {
return this.value[index];
}
}
let a = new FormArray([1, "2", 3]).get("0"); // error
let b = new FormArray([1, "2", 3]).get("1"); // error
let c = new FormArray([1, 2, 3]).get(0); // number
let d = new FormArray([1, "2", 3]).get(1); // string
let e = new FormArray([1, "2", 3]).get(10); // error
Playground Link
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With