Typescript支持通用Map函数

j5fpnvbx  于 2023-05-01  发布在  TypeScript
关注(0)|答案(1)|浏览(224)

在调用Map等Map函数时,是否可以保留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] };

然后,我可以定义一个函数,将这样的更新应用到提供的Map。如果我以下面的方式定义它,似乎一旦我们到达.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不是为此而设计的,所以可能只是使用记录类型的对象。
这里有一个Playground链接

bbmckpt7

bbmckpt71#

问题是编译器无法看到genericTMapsByKind[T]ValueByKind[T]之间的相关性,即使它能够为任何 * 特定 * T(如"string""number")这样做。编译器在抽象推理高阶类型关系方面的能力相当有限。
正因为如此,失败的发生几乎完全像你描述的那样。编译器没有注意到类型为MapsByKind[T]state和类型为ValueByKind[T]value是相关的,因此当您调用state.set(id, value)时,它将state扩展为联合类型Map<number, string> | Map<number, number>,这意味着set也是方法的联合。其为了安全将仅接受参数的交集(如TS 3.3发行说明)。这意味着value需要同时是string * 和 * a number(这是不可能的),但编译器只能确定它是string * 或 * a number,所以它失败了。
microsoft/TypeScript#30581中描述了编译器通常无法查看从同一类型派生的两个表达式之间的相关性。这个问题特别是关于 * 联合类型 * 之间的相关性,这是您的泛型代码所福尔斯的。
幸运的是,有一种方法可以解决这个问题,如microsoft/TypeScript#47109所述。修复方法通常是重构以使用 * 泛型 *(您已经完成了),但操作是根据公共基本类型的mapped types,并根据泛型indexed accesses转换为此类类型。
实际上,您的代码与推荐的执行方式非常相似。ValueByKind类型正是常见的基本类型。而且你的函数在T中是正确的泛型,也就是那种类型的键。唯一的区别是MapsByKind类型是手动写出的,而不是ValueByKind上的Map类型。如果你做了这样的改变:

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属性的Map类型写入时,两者之间的关系对编译器是可见的,而当单独写入时,它是不可见的。
Playground链接到代码

相关问题