如何根据输入为字符串或未定义来创建返回字符串或未定义的打字稿函数?

Posted

技术标签:

【中文标题】如何根据输入为字符串或未定义来创建返回字符串或未定义的打字稿函数?【英文标题】:How to create a typescript function returning a string or undefined based on the input being a string or undefined? 【发布时间】:2021-08-10 18:39:28 【问题描述】:

我试图用纯 javascript 编写的是:

const cleanVar = (var1): T => var1?.replace(searchValue, replaceValue);

在 Typescript 中,我希望能够将其输入为:

const cleanVar = <T extends string | undefined>(var1: T): T => var1?.replace(searchValue, replaceValue);

但我收到此错误:

Type 'string | undefined' is not assignable to type 'T'.
  'string | undefined' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'string | undefined'.
    Type 'undefined' is not assignable to type 'T'.
      'undefined' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'string | undefined'.

那么,输入这个函数的正确方法是什么?

含义应该是:var 可以是stringundefined。如果varstring,则返回类型必须是string。如果varundefined,则返回类型必须是undefined

【问题讨论】:

问题是你的返回类型不是string,而是T,它扩展了stringT 可以是任何东西,但String.prototype.replace 总是返回string,而不是T,所以打字稿不能保证cleanVar 的返回类型是T,因为它知道它是@ 987654343@. 顺便说一句,我认为 TypeScript 根本无法推断出 String.prototype.replace 确实在 Javascript 中返回了 T。请参阅此证明:typescriptlang.org/play?jsx=0#code/… 你当然是对的。 extends 有替代品吗?我可以用它来将 T 限制为 stringundefined 【参考方案1】:

cleanVar() 返回T 是不正确的,因为string literal types 存在,所以如果你调用cleanVar("hello")T 的推断类型将是"hello" 而不是@ 987654338@。即使您可以安排对replace() 的调用,使其在运行时实际上是一个标识函数(例如,v.replace("a","a")),编译器只知道String.prototype.replace() according to the TS standard library 的返回类型是@987654342 @。由于string 包含不可分配给T 的值,因此编译器会正确地报错。


cleanVar() 的“正确”调用签名可能是这样的:

declare const cleanVar: <T extends string | undefined>(v: T) => 
  T extends string ? string : undefined;

也就是说,它应该返回一个conditional type that distributes over unions in T。有了这个签名,让我们验证它在调用时是否按照您想要的方式工作:

const x = cleanVar("hello") // const x: string
const y = cleanVar(undefined) // const y: undefined;
const z = cleanVar(Math.random() < 0.5 ? "hello" : undefined) // const z: string | undefined

看起来不错!


很遗憾,您的实现中仍然会出现编译器错误:

const cleanVar = <T extends string | undefined>(v: T): 
  T extends string ? string : undefined =>
  v?.replace(searchValue, replaceValue); // error!
// Type 'string | undefined' is not assignable to type 'T extends string ? string : undefined'.

这是因为编译器无法真正验证任何特定值是否可分配给conditional type,这取决于未解析的generic 类型参数,例如cleanVar 实现主体中的T。它推迟对这些条件类型的评估,它们仍然是“不透明的”。这是 TypeScript 中的一个已知痛点,microsoft/TypeScript#33912 存在一个未解决的问题,要求提供一些解决方案,从而可以基于 control flow analysis 缩小类型参数 T。现在v?.xyz 将在xyz 内将vT 缩小到string,但是你在函数中所做的任何事情都不能将T 本身从T extends string | undefined 缩小到T extends string。我们被困住了。

目前唯一的方法是使用type assertion;验证实现正确性的责任从编译器(无法做到)转移到您身上......所以要小心。无论如何,它看起来像这样:

const cleanVar = <T extends string | undefined>(v: T) =>
  v?.replace(searchValue, replaceValue) as T extends string ? string : undefined;

另一种“正确”的方法是使 cleanVar() 成为具有多个调用签名的 overloaded 函数,如下所示:

function cleanVar(v: string): string;
function cleanVar(v: undefined): undefined;
function cleanVar(v: string | undefined): string | undefined;
function cleanVar(v: string | undefined) 
  return v?.replace(searchValue, replaceValue);

这与通用条件版本类似,但更冗长。如果没有类型断言(违背目的),你不能轻易地将它变成一个箭头函数,即使使用上面的函数语句,实现中没有错误也类似于使用类型断言......编译器只关心实现签名匹配。如果你把它改成

function cleanVarBad(v: string): string;
function cleanVarBad(v: undefined): undefined;
function cleanVarBad(v: string | undefined): string | undefined;
function cleanVarBad(v: string | undefined): string | undefined 
  return Math.random() < 0.5 ? "oopsie" : undefined;

那么您仍然不会收到错误,但您可能会在运行时非常不开心。因此,对于另一个而言,确实没有太大的好处。我通常更喜欢通用条件调用签名而不是重载。

Playground link to code

【讨论】:

非常有趣和周到的答案!我不明白的一件事是我是否以及为什么真的需要这种重载function cleanVar(v: string | undefined): string | undefined; 如果您有一个v 类型为string | undefined 的变量,您希望能够调用cleanVar(v) 吗?如果是这样,那么您需要单独的重载(请参阅microsoft/TypeScript#14107)。如果没有,那你就没有。

以上是关于如何根据输入为字符串或未定义来创建返回字符串或未定义的打字稿函数?的主要内容,如果未能解决你的问题,请参考以下文章

EXCEL VBA - 对象变量或未设置块变量

Vue.js - this.$refs.key 返回一个空数组或未定义而不是元素

初学者代码问题:“未定义子或函数”

复选框值返回并添加到字符串

为啥 datadog lambda 检测返回“datadog-lambda-js/handler.handler 未定义或未导出”?

如何确定中继查询是不是为空或未加载