Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to restrict method argument to index number of tuple type?

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?

like image 839
John Avatar asked Oct 18 '25 00:10

John


2 Answers

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

like image 125
Titian Cernicova-Dragomir Avatar answered Oct 20 '25 14:10

Titian Cernicova-Dragomir


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

like image 38
Patrick Roberts Avatar answered Oct 20 '25 13:10

Patrick Roberts