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

时间:2019-08-15 12:30:42

标签: typescript typescript-generics

我有一个通用类,其中paramater类型是一个元组。我在类上创建一个方法时遇到了麻烦,该方法的参数仅限于元组的索引。

例如(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”等) )。不幸的是,keyof在元组中引入了 all 所有属性,包括“ length”,“ splice”等。

我尝试使用keyof并排除了所有非number类型的属性,但是后来我意识到索引属性(“ 0”,“ 1”等)由{返回{1}}作为类型keyof

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

更新

要添加到下面接受的答案中,以下是一种解决方法

string

在这里,如果元组的长度为13或更短,我们就可以成功提取出该元组的属性索引号。否则,我们返回通用索引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]; } }

2 个答案:

答案 0 :(得分:2)

您可以将keyof any[]中的keyof T排除在外,而只留下适当的元组键,不幸的是它们将采用字符串形式:

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]返回的索引为I,即使numberkeyof Tstring仍然可以使我惊讶不是number

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

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

答案 1 :(得分:0)

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

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)。