首页 > 解决方案 > 使用参数对函数进行类型推断

问题描述

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

概括地说,这或多或少是我想做的事情:

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 上运行上面的代码时,我遇到了以下错误消息:

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

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

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

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

标签: typescript

解决方案


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

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

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


因此,就目前而言,编写具有通用条件返回类型的函数的唯一方法是使用类型断言(您称之为“类型转换”)或等效的(例如允许您放松实现的单调用签名重载)签名)。

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

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");
        }
    };

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

另请注意return,在不可能的情况下,我是如何抛出错误的default。顺便说一下,这是通用函数实现中控制流的另一个限制。即使有可能在该子句which中缩小到never,编译器甚至都不会尝试,因为它不能缩小T自己。有关支持. _ _ _ _ 你可以在没有 的情况下解决它throw,但我认为这太过分了。

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


对于您编写的特定函数,我知道如何编写它以便编译器实际维护类型安全的唯一方法是将返回类型表示为属性查找

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"并返回类型的值,number或者string好像它正在查找类型对象中的属性NumOrStr。并且实现实际上是这样做的(通过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 代码链接


推荐阅读