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

Posted

技术标签:

【中文标题】TypeScript 数据映射器函数参数从不推断【英文标题】:TypeScript data mapper function argument infers never 【发布时间】: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 【参考方案1】:

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

问题是mapper[key]item[key] 都是union types。 mapper[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

【讨论】:

非常感谢您的详细回答!我知道在 *** 和官方 repo 中肯定有很多类似的问题和问题,但我不知道应该使用什么关键字来搜索它们。从你的链接中,我注意到昨天有一个 PR 旨在解决这个问题。很高兴知道这是 TypeScript 的限制,但不是我的错。再次非常感谢!

以上是关于TypeScript 数据映射器函数参数从不推断的主要内容,如果未能解决你的问题,请参考以下文章

如何在返回其回调之一结果的函数的 Typescript 中声明类型?

TypeScript 从不类型推断

为啥 TypeScript 在使用 concat 减少数组时会推断出“从不”类型?

找不到参数映射器的隐式值

在猪中增加映射器

带有命名参数和行映射器的列表的Java Jdbctemplate查询?