如何将方法参数限制为元组类型的索引号?

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

我有一个泛型 class,其中类型参数是一个元组。我在 class 上创建一个方法时遇到问题,该方法的参数仅限于元组的索引。

例如(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];
  }
}

我知道您可以做的是使用 keyof 获取元组的所有属性,其中将包括与元组包含的对象关联的键(即“0”、“1” , ETC)。不幸的是,keyof 在元组上引入了 所有 属性,包括 "length"、"splice" 等

我尝试使用 keyof 并排除所有非 number 类型的属性,但后来我意识到索引属性(“0”、“1”等)是 return由 keyof 编辑为类型 string.

目前是否可以在 Typescript 中完成此操作?谢谢!

更新

为了补充下面已接受的答案,以下是解决方法

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

在这里,如果元组的长度小于或等于 13,我们可以成功提取元组的 属性 个索引号。否则,我们 return 通用索引 number

您可以从 keyof T 中排除 keyof any[] 并且只剩下适当的元组键,不幸的是它们将是字符串形式:

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

您也可以添加到数字的映射,但恐怕必须手动操作:


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

注意 我很惊讶 T[I] 即使 Inumber 即使 keyof T returns 索引为 string 而不是 number

这种认识使我想到了另一种可能的解决方案,其中 I 也可以是 number。如果数字在元组长度范围内,它将 return 适当的类型,否则它将 return undefined。这不会是调用错误,但是如果您使用 strictNullChecks,return 值将被键入为 undefined,您几乎无能为力:

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

通过引入模板文字字符串,可以用通用形式解决任意长度元组的问题。第一步使用与原始解决方案相同的技术,将元组索引提取为字符串文字的并集:

type Indices<A extends any[]> = Exclude<keyof A, keyof any[]> & string;

诀窍在于第二步:我们不尝试将字符串转换为数字,而是相反:

type numToStr<N extends number> = `${N}`;

现在,我们需要做的就是 Exclude 来自索引联合的操作索引,并检查结果类型是否可分配给原始联合。如果它们不是 - 我们有一个有效的索引,如果它们是 - 索引不是原始联合的一部分:

type onlyValidIndex<A extends any[], I extends number> = Indices<A> extends Exclude< Indices<A>, numToStr<I> > ? never : I;

瞧!适用于您的情况,以下是您将如何使用它:

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

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

const fa = new FormArray([ 0, "", false ]);

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

请注意,您不能再使用索引的字符串文字版本进行索引,但如果需要,只需放宽索引约束以接受辅助类型中的 number | string 联合和 number | Indices<T>get 方法签名中(因为 string 无法索引 T)。