具有通用联合约束的 TypeScript 函数返回值

Posted

技术标签:

【中文标题】具有通用联合约束的 TypeScript 函数返回值【英文标题】:TypeScript function return value with generic union restraint 【发布时间】:2021-03-08 07:11:05 【问题描述】:

我无法理解如何在 TypesScript 中实现泛型。我知道有很多类似的问题,但似乎没有一个可以解决我的用例。 (有this question,但我有一个不同的担心,即函数的输出与函数的输入是同一类型)

我想要一个这样的函数,其中泛型被限制为两种不同的类型,输出与输入的类型相同

const increase = <T extends string | number>(
  param: T,
): T => 
    if (typeof param === "number") 
        // Here typescript should know that param can only be a number
        return param + 1;
    
    // Here TypeScript should know that param can only be a string
    return "one more " + param;
;

const stringResult = increase("apple"); // "one more apple"
// Here TypeScript should know that stringResult is a string

const numResult = increase(2); // 3
// Here TypeScript should know that numResult is a number

但是,这会产生如下错误:

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

但我在上面的if 语句中进行了类型检查,确保T 应该是number 而不是string | number

我希望在 TypeScript 中实现什么?如果是这样,我做错了什么?

【问题讨论】:

问题是,例如,调用increase&lt;4&gt;(4)应该产生4,因为这是通用参数。但是,您 param + 1 不会返回。如果您调用increase&lt;"hello"&gt;("hello"),则与字符串相同 哦,我以为它只是意味着如果通用参数是一个字符串,那么输出也将是一个字符串(并且可能是一个不同的字符串)。有什么办法吗? 【参考方案1】:

如果您不需要函数是箭头函数,您可以轻松地重载该函数:

function increase(param: string): string;

function increase(param: number): number;

function increase(param: string | number): string | number 
    if (typeof param === "number") 
        // Here typescript should know that param can only be a number
        return param + 1;
    
    // Here TypeScript should know that param can only be a string
    return "one more " + param;


const stringResult = increase("apple"); // "one more apple"
// Here TypeScript should know that stringResult is a string

const numResult = increase(2); // 3
// Here TypeScript should know that numResult is a number

Playground Link

这还允许您提供单独的智能感知,详细说明取决于参数类型的行为差异。

如果您绝对需要该函数是箭头函数,请参阅this answer on how to overload an arrow function

因为 javascript 没有类型,所以它不能提供真正的函数重载。为了解决这个问题,TypeScript 允许你提供额外的函数签名来表达重载。函数的实际实现必须手动区分参数类型并选择适当的行为。另请参阅TypeScript Handbook。

【讨论】:

哇!这很完美。我不需要它是箭头函数。如果有一个看起来不那么老套的解决方案会很好吗?找到了关于 TypeScript 函数重载的一个很好的解释here。 为什么你觉得它看起来很老套?这是官方的 TypeScript 重载方法 我想我只是不熟悉重载,所以对我来说似乎很新很奇怪。非常感谢您提供的出色而有帮助的答案。【参考方案2】:

是的,正如您所发现的,TypeScript 编译器不够复杂,无法按照程序的每条路径以这种方式检查类型。 Indexed types 可以解决您的问题,但这些对于您的用例来说还不够灵活。

有一种方法可以严格地做到这一点,但是——请记住——对于某些人来说,这可能有点 Java 风格:访问者模式。

本质上,如果您将每个可能的输入类型包装在一个类中,以便它们都实现一个共享接口,您可以编写访问者对象来执行您需要的任何功能,按类型键入,编译器将确保访问者对象对实现该共享接口的所有类都具有完全类型化的功能。而且看不到ifswitch,所以鲍勃叔叔不会抱怨。

这可能看起来像这样:

interface Increase<T> 
    value: T
    accept(visitor: IncreaseVisitor): T


class NumIncrease implements Increase<number>
    constructor(public value: number) 
    accept(visitor: IncreaseVisitor): number 
        return visitor.visitNum(this)
    


class StrIncrease implements Increase<string>
    constructor(public value: string) 
    accept(visitor: IncreaseVisitor): string 
        return visitor.visitStr(this)
    


interface IncreaseVisitor 
    visitNum(numIncrease: Increase<number>): number
    visitStr(strIncrease: Increase<string>): string


const oneMoreVisitor: IncreaseVisitor = 
    visitNum: (numIncrease: Increase<number>) => numIncrease.value + 1,
    visitStr: (strIncrease: Increase<string>) => "one more " + strIncrease.value


function increase <T>(param: Increase<T>): T 
    return param.accept( oneMoreVisitor )


const stringResult = increase(new StrIncrease("apple"))
console.log(stringResult) // "one more apple"
// Now TypeScript *does* know that stringResult is a string

const numResult = increase(new NumIncrease(2))
console.log(numResult) // 3
// Now TypeScript *does* know that numResult is a number

【讨论】:

哇,非常感谢您出色的解释和实施......“抓住你的帽子”是正确的!这对我来说有点太丑了......

以上是关于具有通用联合约束的 TypeScript 函数返回值的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript 中的函数重载与使用联合类型

映射具有未知深度的通用 TypeScript 接口

typescript :具有原始类型约束的泛型类型

具有通用返回类型的打字稿函数

TypeScript:相当于 C# 的用于扩展类的通用类型约束?

Typescript - 具有通用类型函数的索引签名