【问题标题】:Is it possible to wrap a function and retain its types?是否可以包装函数并保留其类型?
【发布时间】:2016-07-26 19:23:39
【问题描述】:

我正在尝试创建一个通用包装函数,它将包装传递给它的任何函数。

在最基本的包装函数看起来像

function wrap<T extends Function>(fn: T) {
    return (...args) => {
        return fn(...args)
    };
}

我正在尝试像这样使用它:

function foo(a: string, b: number): [string, number] {
    return [a, b];
}

const wrappedFoo = wrap(foo);

现在wrappedFoo 的类型是(...args: any[]) =&gt; any

是否可以让wrappedFoo 模仿其包装的函数类型?

【问题讨论】:

    标签: typescript


    【解决方案1】:

    这适用于任意数量的参数,并保留所有参数和返回类型

    const wrap = <T extends Array<any>, U>(fn: (...args: T) => U) => {
      return (...args: T): U => fn(...args)
    }
    

    【讨论】:

    • 我对此进行了测试并且它有效,但我不确定为什么。这里有T 的名字吗?我希望&lt;T, U extends Array&lt;T&gt;&gt; 表示“T 类型的同质元素的数组”,或者,如果参数为[string, number]Array&lt;string|number&gt;Array&lt;T&gt; 怎么能表示“按特定顺序排列的特定系列类型的数组”?
    • 我认为不需要T...&lt;U extends any[], V&gt; 对于这个签名应该足够了
    • @ElliotNelson @jcalz 是的,可以改用Array&lt;any&gt;,我是用T来绕过我的eslint警告,但是Array&lt;any&gt;更清晰,谢谢你的反馈!
    • 内部函数的显式返回类型,: U 让我有点困惑。我认为可以从参数类型推断? playground
    • 我发现将 Array&lt;any&gt; 更改为 Array&lt;unknown&gt; 可以避免 linting 规则 no-explicit-any
    【解决方案2】:

    通过进行 2 次更改,可以创建一个包装函数,该函数接受并返回与其包装的函数相同的类型

    1. 将包装函数的返回值指定为要包装的T 泛型
    2. 将您要返回的函数转换为&lt;any&gt;

    例如:

    function wrap<T extends Function>(fn: T): T {
        return <any>function(...args) {
            return fn(...args)
        };
    }
    

    然后const wrappedFoo = wrap(foo);的类型

    然后正确地是:

    (a: string, b: number) => [string, number].
    

    【讨论】:

    • ...args 在 Typescript 中生成 for loop,您可以通过使用函数局部变量 arguments 来避免这种情况。
    • 太好了!对于返回与被包装函数相同的值但不直接返回函数调用的包装器是否有类似的可能?
    • 这真是太棒了。一个小问题是,强制转换为 any 确实会破坏返回值的类型测试,这意味着如果您的包装器错误地返回的类型与被包装的函数类型不同,打字稿将无法捕获它。
    • 我正在寻找一个类似的实现,但是fn 是可选的并且默认为() =&gt; {}。请问这样可以吗?
    【解决方案3】:

    最近需要用箭头函数来做这件事,想出了一种比其他一些答案更容易工作的方法,并解决了扩展运算符和 Symbol.iterator 的问题(在 ts 3.8 中测试)

    type AnyFunction = (...args: any[]) => any;
    
    const wrap = <Func extends AnyFunction>(
      fn: Func,
    ): ((...args: Parameters<Func>) => ReturnType<Func>) => {
      const wrappedFn = (...args: Parameters<Func>): ReturnType<Func> => {
          // your code here
          return fn(...args);
      };
      return wrappedFn;
    };
    
    • AnyFunction 是必需的,所以我们可以使用 rest 运算符并提取参数类型/返回类型
      • 使用Func extends AnyFunc 然后让我们描述我们特别喜欢的任何函数的类型
    • 仅仅说结果是Func 给出了包装函数无法用不同的子类型实例化的问题,因此我们需要重新构造它
      • Parameters&lt;Func&gt; 获取任意参数作为元组
        • Symbol.iterator 问题的解决方法是使用...Array.from(args) 而不是...args
      • ReturnType&lt;Func&gt; 获取任意返回类型
    • 您的 IDE 应该正确识别生成的包装函数的所有类型

    包装函数现在将与内部函数具有相同的签名,几乎可以做任何事情

    const foo = (a: string, b: number): number => a.length + b;
    const wrappedFoo = wrap<typeof foo>(foo);
    
    foo('hello', -5); // => 0
    wrappedFoo('hello', -5); // => 0
    

    【讨论】:

    • 在 VSCode 中效果很好!甚至保留参数的签名/名称
    • 如果不想使用any类型,可以使用(...args: never[]) =&gt; unknown代替(...args: any[]) =&gt; any
    • @ScottyJamison 为什么在这种特定情况下不使用any,这难道不是any 打算使用的方式之一吗?包装后的函数将使用与原始函数相同的签名进行完全类型化。
    • any 类型基本上要求类型检查器关闭对该值的类型检查。当没有其他选择时,这是一个非常有用的逃生舱,通常是因为您正在与缺少类型定义的代码集成,或者处理 JS 的动态方面。这就是为什么其他类型安全语言不需要 any 类型的原因。包括我在内的一些人喜欢尝试避免使用 any 类型,除非真的没有其他选择,因为它有助于鼓励代码更加安全。由于这里有一种避免它的简单方法,因此我认为没有特别的理由使用它。
    • 这感觉像是在滥用never,因为const foo: never[] = [1]; 是无效的,这意味着任何参数在技术上都与提供的类型不匹配。另外unknown 表示返回类型与Type 'unknown' is not assignable to type 'ReturnType&lt;Func&gt;' 不一样。是的,any 在很多情况下“基本上要求类型检查器关闭”,但在这种情况下,它是在受控情况下,我们定义类型可能是什么而不是类型是什么的限制
    【解决方案4】:

    这是可能的,但如果您希望能够传递不同类型和数量的参数,可能会有点混乱。

    您的示例可以这样完成:

    function wrap<A, B, C>(fn: (a: A, b: B) => C) {
        return (a: A, b: B): C => {
            return fn(a, b);
        };
    }
    

    然后是类型:

    const wrappedFoo = wrap(foo);
    

    (a: string, b: number) =&gt; [string, number]
    (code in playground)

    但是正如您所看到的,如果您希望能够使用不同的签名(例如,我的示例仅适用于两个参数),那么使用它并不是很舒服。

    你可以做的是只传递一个由接口支持的参数:

    function wrap<In, Out>(fn: (params: In) => Out) {
        return (params: In): Out => {
            return fn(params);
        };
    }
    
    interface FooParams {
        a: string;
        b: number;
    }
    
    function foo(params: FooParams): [string, number] {
        return [params.a, params.b];
    }
    
    const wrappedFoo = wrap(foo);
    

    (code in playground)

    在我看来,这将更容易使用。

    【讨论】:

    • 感谢您的回答,我宁愿不强制每个被包装的函数都接受一个 params 对象...我一直在搞乱,并认为我有解决这个特定问题的方法...
    【解决方案5】:

    您可以使用overloads 提供特定类型,用于包装具有 0、1、2、3、4 或更多参数的函数。如果您的某个函数需要更多参数,请添加一个额外的重载或只是让它回退到其余参数的情况。

    function wrap<TResult>(fn: () => TResult) : () => TResult;
    function wrap<T1, TResult>(fn: (param1 : T1) => TResult) : (param1 : T1) => TResult;
    function wrap<T1, T2, TResult>(fn: (param1 : T1, param2 : T2) => TResult) : (param1 : T1, param2 : T2) => TResult;
    function wrap<T1, T2, T3, TResult>(fn: (param1 : T1, param2 : T2, param3 : T3) => TResult) : (param1 : T1, param2 : T2, param3 : T3) => TResult;
    function wrap<T1, T2, T3, T4, TResult>(fn: (param1 : T1, param2 : T2, param3 : T3, param4 : T4) => TResult) : (param1 : T1, param2 : T2, param3 : T3, param4 : T4) => TResult;
    function wrap<TParam, TResult>(fn: (...params : TParam[]) => TResult) : (...params : TParam[]) => TResult {
        return (...params) => {
            return fn(...params);
        };
    }
    

    它不是很漂亮,但它确实给出了最准确的类型。

    【讨论】:

      猜你喜欢
      • 2023-04-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多