TypeScript - 受限元组泛型的类型保存映射类型

TypeScript - typesave mapping types of restricted tuple generic

在 TypeScript 中,这不是编译:

export interface Generic<T extends string> {

}

export interface Class<T extends string[]> {
  readonly prop: { [P in keyof T]: Generic<T[P]> }
}

特别是 Generic<T[P]> 失败并显示 Type 'T[P]' does not satisfy the constraint 'string'.。然而,由于T extends string[],可以肯定的是,对于任何P in keyof T.

T[P] extends string

我做错了什么?


我知道我可以用条件类型解决问题:

export interface Class<T extends string[]> {
  readonly prop: { [P in keyof T]: T[P] extends string ? Generic<T[P]> : never }
}

但我不明白为什么这是必要的。

如果你看完整的错误,第三行有一个大线索:

Type 'T[P]' does not satisfy the constraint 'string'.
  Type 'T[keyof T]' is not assignable to type 'string'.
    Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'string'.
      Type 'T[string]' is not assignable to type 'string'.(2344)

问题是 keyof 任何数组类型(或元组类型)都会更像 string | number | symbol。一个数组在这些键上也有比它更多的成员类型。例如:

// (...items: string[]) => number
type PushFunction = string[]['push']

查看此代码段。数组键中不仅仅是数字:

// number | "0" | "1" | "2" | "length" | "toString"
// | "toLocaleString" | "pop" | "push" | "concat"
// | "join" | "reverse" | "shift" | "slice" | "sort"
// | "splice" | "unshift" | "indexOf"
// | ... 15 more ... | "includes"
type ArrayKeys = keyof [1,2,3]

Generic<T>要求T是一个字符串,但是,如图所示,并非数组所有键的所有值都是字符串。

Playground


您可以通过将数组键与 number 相交来非常简单地修复映射类型,通知打字稿您只关心数字键(这是数组索引):

export interface Generic<T extends string> {

}

export interface Class<T extends string[]> {
  readonly prop: { [P in keyof T & number]: Generic<T[P]> }
}

Playground

这是 TypeScript 中的一个已知错误,其中支持(在 TS3.1 中添加)mapped types over tuples and arrays does not exist inside the implementation of such mapped types; see microsoft/TypeScript#27995. It seems that according to TypeScript 的首席架构师:

The issue here is that we only map to tuple and array types when we instantiate a generic homomorphic mapped type for a tuple or array (see #26063).

所以从外部,当你在数组或元组上使用映射类型时,映射会保留数组-或-输入的元组性,仅映射到数字属性:

declare const foo: Class<["a", "b", "c"]>;
// (property) prop: [Generic<"a">, Generic<"b">, Generic<"c">]
const zero = foo.prop[0]; // Generic<"a">;
const one = foo.prop[1]; // Generic<"b">;

但在内部,编译器仍然将P in keyof T视为遍历T的每个键,包括任何可能的非数字。

export interface Class<T extends string[]> {
  readonly prop: { [P in keyof T]: Generic<T[P]> } // error!
}

如您所见,有解决此问题的方法,microsoft/TypeScript#27995 中提到了这些解决方法。我认为最好的是与你的条件类型基本相同:

export interface Class<T extends string[]> {
  readonly prop: { [P in keyof T]: Generic<Extract<T[P], string>> }
}

其中的其他类型要么不适用于像 T 这样的泛型类型,要么生成不再是真正的数组或元组的映射类型(例如,{0: Generic<"a">, 1: Generic<"b">, 2: Generic<"c">} 而不是 [Generic<"a">, Generic<"b">, Generic<"c">]...所以我将把它们排除在这个答案之外。

Playground link to code

请注意,对于 T extends string[]keyof T 不仅包括数字键,还包括构成 Array 原型的所有方法。 证明?这是:

type StringArrayKeys = keyof string[];
// Produces: 
// type StringArrayKeys = number | "length" | "toString" | "toLocaleString" | "pop" | 
//   "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" | 
//   "unshift" | "indexOf"  | "lastIndexOf" | ... 16 more

因此,对于您的示例,最简单的修复方法是将 P in keyof T 替换为 P in number:

export interface Generic<T extends string> {};

export interface Class<T extends string[]> {
    readonly prop: { [P in number]: Generic<T[P]> }
}