【问题标题】:How to properly type a higher order function如何正确输入高阶函数
【发布时间】:2017-02-22 14:13:04
【问题描述】:

我有一个高阶函数,例如在 vanilla JS 中是这样的:

function log(fn) {
  return (...args) => {
    console.log("Calling", fn.name);
    return fn(...args);
  }
}

并像这样使用:

let name;
const setName = log((newName) => {
   name = newName;
});
setName("Hello"); // output "Calling setName"

我怎样才能在 TS 中正确键入这个,以使 setName 具有正确的函数签名?含义:

setName("hello") // OK
setName() // Error, missing arg
setName(123) // Error, number not string

到目前为止,我想出的是:

function log<F extends Function>(fn: F): F {
  return (...args: any[]) => {
    console.log("Calling", fn.name);
    return fn(...args);
  }
}

let name;
const setName = log((newName: string) => {
  name = newName;
});

按我想要的方式工作(我在 setName 上进行参数类型检查),但在 log 函数返回时出现编译错误:

错误:TS2322:Type '(...args: any[]) => any' 不可分配给类型 'F'。

即使我使用&lt;F extends (...args: any[]) =&gt; any&gt;,我也会遇到同样的错误。基本上不知道如何让返回的函数满足泛型类型F

我该怎么做?

【问题讨论】:

    标签: typescript


    【解决方案1】:

    带有函数重载

    Function overloads 可以使用。这是一种将定义与实现分开的方法:

    function log<F extends Function>(fn: F): F
    function log(fn: Function) {
        return (...args) => {
            console.log("Calling", fn.name);
            return fn(...args);
        }
    }
    

    我喜欢重载版本,因为它有助于在模块作为预编译 NPM 包分发时生成 TypeScript 定义。

    带有类型断言as F

    Function 类型太宽。您的包装函数必须是以下子类型:(...args: any[]) =&gt; any

    您可以使用as F 来告诉 TypeScript 对返回的函数有信心:

    function log<F extends (...args: any[]) => any>(fn: F): F {
        return ((...args: any[]) => {
            console.log("Calling", fn.name);
            return fn(...args);
        }) as F
    }
    

    带有类型断言as any

    相同的实现,但我们可以使用 Functionas any 代替 (...args: any[]) =&gt; anyas F(参见 Louis 的回答)。

    【讨论】:

    • 谢谢,我没有想到使用这样的函数重载。
    • 我根据其他贡献者的想法进行了编辑。 Function 类型太宽。您的包装函数的类型必须为:(...args: any[]) =&gt; any
    • 有趣,我用我的原始代码尝试了F extends (...args: any[]) =&gt; any,但由于某种原因,即使使用那个确切的签名,它仍然不允许我返回,并且as F 不适用于F extends Function .老实说,我不明白为什么这是不同的,但它确实有效。
    • 我的想法是:Function 是对象,它们可以有额外的成员。比如一个函数接口:interface setNameFn { (data: string): void; otherMember: string; }。您的包装器没有实现这种情况,我想为编译器提供一种控制这种情况的方法。但是,经过一些测试,log 似乎接受了setNameFn 类型的函数。因此,更好的选择可能是最容易阅读的,使用Functionas any 而不是(...args: any[]) =&gt; anyas F
    【解决方案2】:

    您可以进行的最直接的修改是将返回值的类型强制为any

    function log<F extends Function>(fn: F): F {
      return ((...args: any[]) => {
        console.log("Calling", fn.name);
        return fn(...args);
      }) as any;
    }
    

    这会编译文件,并且这样做,您的 setName 函数将具有与您传递给 log 的签名相同的签名。

    【讨论】:

    • 谢谢,我发现这行得通,但我不喜欢使用as any... 还有,为什么as Function 不行?
    • 使用 as any 并不比在另一个答案中使用 as F 差。无论哪种方式,您都是在告诉编译器“相信我,我知道我在做什么”。 as Function 无法工作,因为 F 由您传递的参数确定。到log。如果FFunction 的扩展,则将F 类型的对象分配给可以接受Function 类型的对象的变量是完全可以的,但是将Function 类型的对象分配给可以接受的变量F 类型的对象不行。如果您使用as Function 而不是as any,那么您实际上是在尝试执行后者。
    • 我明白了,谢谢你的解释。基本上这是因为您不能将基本类型分配给子类型。
    猜你喜欢
    • 2013-05-07
    • 2022-12-04
    • 1970-01-01
    • 1970-01-01
    • 2021-04-09
    • 2020-10-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多