TypeScript 数据映射器函数参数从不推断

TypeScript data mapper function argument infers never

考虑以下示例:

type columns = {
    A: number;
    B: string;
    C: boolean;
};
const mapper: { [T in keyof columns]: (item: columns[T]) => string } = {
    A: item => `${item}`,
    B: item => item,
    C: item => (item ? "Good" : "Bad"),
};
const data: columns[] = [
    { A: 0, B: "Hello", C: true },
    { A: 1, B: "World", C: false },
];
const keys: (keyof columns)[] = ["A", "B", "C"];
data.map(item => keys.map(key => mapper[key](item[key] as never)));
// should return [["0", "Hello", "Good"], ["1", "World", "Bad"]]

在最后一行,key是类型keyof columns,即"A" | "B" | "C",这使得mapper[key]减少到(item: number & string & boolean) => string,即[=16] =]. (如果我的概念有误请指出。)

所以问题是,我如何重写代码使得 item[key] 不需要转换为 never

这是 TypeScript 的一般限制,microsoft/TypeScript#30581. The compiler is really not able to look at a single expression like mapper[key](item[key]) and use control flow analysis 的主题是分析它是否安全。

问题是 mapper[key]item[key] 都是 union typesmapper[key]的类型是((item: number) => string) | ((item: string) => string) | ((item: boolean)=>string)item[key]的类型是number | string | boolean。但是编译器没有很好的方法来跟踪这些值的类型之间的相关性。它将所有工会视为基本上彼此独立。我们知道当 item[key]numbermapper[key](item: number) => string,但编译器不知道。就其理解而言,mapper[key] 可以接受 numberitem[key]string。相关性与我们在两个表达式中使用相同的 key 这一事实有关,但编译器仅跟踪 key 的类型,而不是其身份。

当你独立对待 mapper[key]item[key] 时,你有点卡住了。您只能调用参数的函数类型 with arguments that would work for every member of the union. That is, an intersection 的联合...因此编译器将 mapper[key] 视为可分配给 (item: number & string & boolean) => string,又名 (item: never) => string... 意思是这样的调用函数通常是不安全的。

直到并且除非有某种方法告诉编译器跟踪联合类型表达式之间的相关性,否则没有很好的方法可以继续。如果你最关心类型安全,你可以写一些冗余代码来获得它:

data.map(item => keys.map(key =>
  key === "A" ? mapper[key](item[key]) :
    key === "B" ? mapper[key](item[key]) :
      mapper[key](item[key])
)); // no error but it's redundant and repetitive 
// and also redundant

如果您关心的是方便而不是类型安全,那么您可以使用这样的 type assertion to suppress errors. Your example of item[key] as never is one way to do it, although you're technically lying about what item[key] is. If you don't want to lie you can use a generic 回调函数:

data.map(item => keys.map(<K extends keyof Columns>(key: K) =>
  (mapper[key] as (item: Columns[K]) => string)(item[key]) // okay
));

您必须断言 mapper[key] 是类型 (item: Columns[K]) => string) 的值,因为编译器无法验证这一点,即使理论上它应该能够验证。当您尝试调用它时,它会急切地将 mapper[key] 解析为一个函数联合。由于 mapper[key] 确实是该类型的值,所以我们没有撒谎。这里缺乏类型安全是因为如果有人恶意切换 mapper 的条目,编译器不会注意到:

const evilMapper = {
  A: mapper.B,
  B: mapper.C,
  C: mapper.A
}

data.map(item => keys.map(<K extends keyof Columns>(key: K) =>
  (evilMapper[key] as (item: Columns[K]) => string)(item[key]) // okay?!
));

而冗余版本会开始尖叫:

data.map(item => keys.map(key =>
  key === "A" ? evilMapper[key](item[key]) : // error
    key === "B" ? evilMapper[key](item[key]) : // error
      evilMapper[key](item[key]) // error
)); 

Playground link to code