【问题标题】:TypeScript data mapper function argument infers neverTypeScript 数据映射器函数参数从不推断
【发布时间】:2022-01-14 19:21:09
【问题描述】:

考虑以下示例:

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,即(item: never) => string。 (如果我的概念是错误的,请指出。)

所以问题是,我怎样才能重写代码,使item[key] 不需要转换为never

【问题讨论】:

  • 类型名称通常用 UpperPascalCase 编写,所以我在回答中将columns 切换为Columns。此外,键类型的类型参数更传统地命名为K(用于键)或P(用于属性),因此我在答案代码中也将T in keyof columns 切换为K in keyof Columns

标签: typescript types type-inference


【解决方案1】:

这是 TypeScript 和 microsoft/TypeScript#30581 主题的一般限制。编译器确实无法查看像mapper[key](item[key]) 这样的单个表达式并使用control flow analysis 对其进行分析以查看它是否安全。

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

当您单独对待 mapper[key]item[key] 时,您会陷入困境。您只能调用函数类型的联合with arguments that would work for every member of the union。也就是说,参数的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 来抑制错误。您的 item[key] as never 示例是一种方法,尽管您在技术上对 item[key] 是什么撒谎。如果你不想撒谎,你可以像这样使用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]) =&gt; 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

【讨论】:

  • 非常感谢您的详细回答!我知道在 StackOverflow 和官方 repo 中肯定有很多类似的问题和问题,但我不知道应该使用什么关键字来搜索它们。从你的链接中,我注意到昨天有一个 PR 旨在解决这个问题。很高兴知道这是 TypeScript 的限制,但不是我的错。再次非常感谢!
猜你喜欢
  • 1970-01-01
  • 2018-11-19
  • 1970-01-01
  • 2018-11-01
  • 1970-01-01
  • 2023-02-26
  • 2016-11-01
  • 2020-03-11
  • 1970-01-01
相关资源
最近更新 更多