TypeScript 对通用 Map 函数的支持
在调用 Map 函数(例如 Map.set)时,有可能保留 TypeScript 泛型索引吗?假设我已经设置了这些类型。
type DataKind = "string" | "number";
type MapsByKind = {
string: Map<number, string>;
number: Map<number, number>;
};
type ValueByKind = {
string: string;
number: number;
};
然后我添加一个通用的更新类型,比如
type UpdatePayload<T extends DataKind> = { readonly id: number; value: ValueByKind[T] };
那么我可以定义一个函数,将这样的更新应用到提供的映射上吗。如果我按以下方式定义它,似乎一旦我们到达 .set 部分,泛型类型就被视为联合类型,或类似的东西,不再是泛型。
const applyUpdate = <T extends DataKind>(
state: MapsByKind[T],
payload: UpdatePayload<T>
) => {
const {id,value}=payload; index by generic = ValueByKind[T]
state // MapsByKind[T]
const setter = state.set // no longer generic but union
const next = state.set(id,value) // I guess intersection
};
我猜 TypeScript 不是为这个设计的,所以可能只会使用用 Record 类型的对象。
这是一个游乐场链接
答案#1
问题在于,编译器无法看到泛型 T 的 MapsByKind[T] 和 ValueByKind[T] 之间的相关性,尽管它能够对任何 具体的 T(例如 "string" 或 "number")做到这一点。编译器在抽象推理高阶类型关系方面的能力相当有限。
正因为如此,失败几乎完全如你所描述的那样发生。编译器没有注意到类型为 MapsByKind[T] 的 state 与类型为 ValueByKind[T] 的 value 是相关的,所以当你调用 state.set(id, value) 时,它会将 state 扩展为联合类型 Map<number, string> | Map<number, number>,这意味着 set 也成为方法的联合类型,为了安全,它只能接受参数的交集(如 TS3.3 发布说明中所述)。这意味着 value 需要同时是 string 和 number(这是不可能的),但编译器只能确定它是 string 或 number,所以编译失败。
编译器普遍无法看到源自相同类型的两个表达式之间的相关性,这在 microsoft/TypeScript#30581 中有所描述。该问题特别涉及 联合类型 之间的相关性,这正是你的泛型代码所退回的类型。
幸运的是,有一种方法可以解决它,如 microsoft/TypeScript#47109 所描述。通常的修复方法是重构以使用 泛型(你已经完成了),但操作是在针对一个共同基础类型的映射类型的情况下,并且是在对这些类型的泛型索引访问的基础上进行的。
实际上,你的代码与推荐的方法非常相似。ValueByKind 类型正是常见的基础类型。而你的函数在 T 上是正确的泛型,即该类型的键。唯一的区别是你的 MapsByKind 类型是手动写出的,而不是作为 ValueByKind 的映射类型。如果你做出这个更改:
type MapsByKind = {
[K in keyof ValueByKind]: Map<number, ValueByKind[K]>
};
/* type MapsByKind = {
string: Map<number, string>;
number: Map<number, number>;
} */
然后突然一切都正常了:
const applyUpdate = <T extends DataKind>(
state: MapsByKind[T],
payload: UpdatePayload<T>
) => {
const { id, value } = payload;
// const value: ValueByKind[T]
const setter = state.set
// const setter: (key: number, value: ValueByKind[T]) => MapsByKind[T]
const next = state.set(id, value); // okay!
};
state.set 的类型现在被视为单一的、非联合的泛型函数类型 (key: number, value: ValueByKind[T]) => MapsByKind[T],并且由于 value 被视为 ValueByKind[T],调用成功了!
注意问题及其解决方案是多么微妙。MapsByKind 类型在两种情况下都是等价的,当你使用 IntelliSense 显示该类型时,显示是相同的。但是,该类型的内部表示却有关键区别:当 ValueByKind 写成对 MapsByKind 属性的映射类型时,两者之间的关系对编译器是可见的,而当独立编写时,该关系对编译器是不可见的。
代码的 Playground 链接
