使用参数对函数进行类型推断

Posted

技术标签:

【中文标题】使用参数对函数进行类型推断【英文标题】:Type Inference on a function using a parameter 【发布时间】:2021-04-23 01:20:17 【问题描述】:

我最近一直在摆弄泛型类型,在编写一个函数时,我必须使用函数中的一个参数来确定返回类型。

为了给出一个总体思路,这里或多或少是我想做的事情:

const getString = () => 'string';

const getNumber = () => 9999;

const getNumberOrString =
    <
        T extends 'number' | 'string',
        ReturnType extends T extends 'number' ? number : T extends 'string' ? string : void
    >(which: T): ReturnType => 
        switch (which) 
            case 'number':
                return getNumber();
            case 'string':
                return getString();
            default:
                return;
        
    ;

在 Typescript Playground 上运行上述代码时,我遇到以下错误消息:

类型“数字”不可分配给类型“ReturnType”。 “number”可分配给“ReturnType”类型的约束,但“ReturnType”可以用约束“string |”的不同子类型来实例化。号码'。(2322)

类型“字符串”不可分配给类型“ReturnType”。 “string”可分配给“ReturnType”类型的约束,但“ReturnType”可以用约束“string |”的不同子类型来实例化。号码'。(2322)

类型“未定义”不可分配给类型“ReturnType”。 'ReturnType' 可以用与 'undefined' 无关的任意类型实例化。(2322)

我可以让这段代码在没有任何错误的情况下运行的唯一方法是将返回语句类型转换为any,但是有没有其他方法可以做到这一点而不必求助于类型转换?

【问题讨论】:

您好!感谢您的贡献。供日后参考,请don't post error messages as screenshots。我刚刚提交了一个修改,将您的屏幕截图更改为错误消息的实际文本内容。 谢谢,下次我会记住的。 【参考方案1】:

这是 TypeScript 中的一个已知限制;对依赖于未指定泛型类型参数的条件类型的评估是延迟,编译器不知道如何验证值是否可以分配给它们。

getNumberOrString() 的实现中,泛型类型参数T 未指定(它仅在调用 getNumberOrString() 时指定),因此返回类型T extends 'number' ? number : ... 是其中之一这些延迟类型。因此,当您尝试返回任何特定值(例如 return getNumber())时,编译器无法验证 number 是否可分配给该类型,并且您会收到错误消息。

如果编译器可以使用控制流分析之类的东西来理解return getNumber() 只能在T extends 'number' 时发生,那就太好了,但目前情况并非如此。请参阅microsoft/TypeScript#33912 以获取对此实现支持的功能请求,并讨论为什么这不是一个容易解决的问题。


所以现在,编写具有通用条件返回类型的函数的唯一方法是使用type assertions(你称之为“类型转换”)或等效的(如单一调用签名overload允许您放松实现签名)。

以下是您编写函数的方式:

type NumOrStrReturnType<T> = 
  T extends 'number' ? number : 
  T extends 'string' ? string : 
  never;

const getNumberOrString =
    <T extends 'number' | 'string'>(which: T): NumOrStrReturnType<T> => 
        switch (which) 
            case 'number':
                return getNumber() as NumOrStrReturnType<T>;
            case 'string':
                return getString() as NumOrStrReturnType<T>;
            default:
                throw new Error("I DIDN'T EXPECT THAT");
        
    ;

注意getNumberOrString 只有一个 通用参数。看起来您使用第二个泛型参数作为为返回类型提供短名称的一种方式,但它可能导致奇怪的行为(例如,它可以指定为比预期更窄的类型(例如,getNumberOrString&lt;'number',42&gt;('number'))所以我要避免这种情况。相反,我只是为返回类型提供了一个类型别名,并在类型断言中多次使用它。

另请注意,我在不可能的default 案例中抛出了一个错误,而不是return。顺便说一下,这是通用函数实现中控制流的另一个限制。即使which 有可能在该子句中缩小为never,编译器甚至都不会尝试,因为它本身不能缩小T。请参阅microsoft/TypeScript#13995 和microsoft/TypeScript#24085 以了解支持that 的功能请求。你可以在没有throw 的情况下解决它,但我认为这太过分了。

这里的要点是,任何使用泛型条件类型的getNumberOrString() 版本都需要您作为实现者负责维护类型安全,因为从 TypeScript 4.1 开始,编译器无法胜任该任务。


对于您编写的特定函数,我知道如何编写它以便编译器真正维护类型安全的唯一方法是将返回类型表示为property lookup:

interface NumOrStr 
    number: number;
    string: string;

const getNumberOrString2 =
    <K extends keyof NumOrStr>(which: K): NumOrStr[K] 
        return (
            get number()  return getNumber() ,
            get string()  return getString() 
        )[which];
    

编译器将getNumberOrString2() 视为获取"number""string" 类型的参数并返回numberstring 类型的值,就好像它正在查找@987654352 类型的对象中的属性一样@。并且实现实际上是这样做的(通过getter)。这可以按需要工作:

console.log(getNumberOrString2("number").toFixed(2)); // 9999.00
console.log(getNumberOrString2("string").toUpperCase()); // STRING
const sOrN = getNumberOrString2(Math.random() < 0.5 ? "string" : "number");
// const sOrN: string | number

是否值得跳过这些障碍从编译器中获得类型安全?可能不是。但至少可能在这里做,我猜这很好。


Playground link to code

【讨论】:

以上是关于使用参数对函数进行类型推断的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin函数 ⑤ ( 匿名函数变量类型推断 | 匿名函数参数类型自动推断 | 匿名函数又称为 Lambda 表达式 )

现代C++之理解auto类型推断

打字稿泛型:从函数参数的类型推断类型?

对 Haskell 类型推断感到困惑

高阶函数参数类型的错误类型推断

从函数回调推断泛型类型参数