【问题标题】:How to restrict method argument to index number of tuple type?如何将方法参数限制为元组类型的索引号?
【发布时间】:2019-08-15 12:30:42
【问题描述】:

我有一个泛型类,其中类型参数是一个元组。我无法在具有限制为元组索引的参数的类上创建方法。

例如(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 在元组中引入了 所有 属性,包括“长度”、“拼接”等。

我尝试使用keyof 并排除所有不属于number 类型的属性,但后来我意识到keyof 将索引属性(“0”、“1”等)作为类型返回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,我们可以成功提取元组的属性索引号。否则,我们返回通用索引number

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案1】:

    您可以从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 返回索引为 string 而不是 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

    【讨论】:

    • 谢谢!我在 typescript repo 中创建了一个功能请求以某种方式来实现这一点(也许只是将"0" 转换为0 的能力):github.com/microsoft/TypeScript/issues/32917
    • 我根据您的建议提出了一个很好的解决方法,并将其添加到原始问题的底部。
    【解决方案2】:

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

    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 联合和 get 方法中的 number | Indices&lt;T&gt;签名(因为string 不能索引T)。

    【讨论】:

      猜你喜欢
      • 2022-06-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多