具有通用类型的 Typescript 包装函数

Typescript wrapping function with generic type

如何在不更改 Typescript 中的通用类型的情况下包装函数?

function x() {
  console.log('Original Function');
}

function wrapper<T extends Function>(func: T): T {
  // Typescript compiler error:
  // Type '() => void' is not assignable to type 'T'.
  return () => {
    console.log('Wrapped Function');
    func.call(null);
  }
}

const xWrapped = wrapper(x);
xWrapped(); // logged 'Wrapped Function' & 'Original Function'

包装函数应该接受一个函数和return一个具有完全相同类型签名的函数。

Typescript 无法知道传递的函数有多少个参数或者它 return 是什么,你的函数隐含地假设它有 0 个参数(除了这个)并且 return 无效。

目前没有一个很好的方法可以在打字稿中保存函数签名。然而,有一个针对更高版本的新提案可能会解决此问题:https://github.com/Microsoft/TypeScript/issues/5453

现在你可以制作一个看起来像这样的通用包装器。

function wrapper<T extends (...args:any[])=>any>(func: T): T {
  return <T>((...args:any[]) => {
        console.log('Wrapped Function');
        return func(...args);
    });
}

根据 Variadic 类型的提议,这个函数可以这样写

function wrapper<...TArgs,TRet>(func:(...args:...TARGS)=>TRet) {
    return (...args:...TARGS) => {
        console.log("Wrapped function");
        return func(...args);
    }
}

请注意,这里的主要区别在于没有强制转换,上面的解决方案必须告诉编译器 return 变量与输入变量的类型相同。然而,对于可变类型,参数本身可以被通用地键入。 (注意可变参数类型目前不在 typescript 中,当包含 do 时,上面的代码完全有可能有语法错误)

TypeScript 4.3 版本更新: 包装器的实施提案工作代码看起来像

type InferArgs<T> = T extends (...t: [...infer Arg]) => any ? Arg : never;
type InferReturn<T> = T extends (...t: [...infer Arg]) => infer Res ? Res : never;

function getWrapper<TFunc extends (...args: any[]) => any>(func: TFunc)
    : (...args: InferArguments<TFunc>) => InferReturn<TFunc> {

    return (...args: InferArguments<TFunc>) => {
        // something before

        try {
            return func(...args);
        } finally {

            // something after;
        }
    };
}

可以在此处找到有关使用可变参数元组的一些简短但有用的信息和示例:https://fettblog.eu/variadic-tuple-types-preview/

这是一个保留内部函数的参数和 returns 类型而不依赖于可变参数类型的替代方法。

function x(message: string): void {
    console.log(`inner ${message}`);
}

export function wrapper<Args extends any[], Return>(
    operation: (...operationParameters: Args) => Return, 
    ...parameters: Args
): Return {
    console.log(`outer `);

    return operation(...parameters);
}

x("abc");
wrapper(x, "xyz");

// output:
//
// inner abc
// outer
// inner xyz

当用 x 调用 wrapper 时,TS 编译器将其类型推断为 function wrapper<[string], void>(operation: (operationParameters_0: string) => void, parameters_0: string): void

如果您尝试调用 wrapper(x, 123),它会因类型安全而失败:Argument of type '123' is not assignable to parameter of type 'string'.

我更喜欢更简单的解决方案,如下所示:


function wrapFunction<TArgs extends any[], TReturn>(
  targetFunction: (...parameters: TArgs) => TReturn,
): (...parameters: TArgs) => TReturn {
  return (...parameters: TArgs) => {
    console.log(`Hello, what is your name?`);
    return targetFunction(...parameters);
  };
}


// --------------- Example
const someFunction = (name: string) => {
    console.log(`Hey! My name is ${name}.`);
}

const wrappedFunction = wrapFunction(someFunction);
wrappedFunction("Fábio");

输出

[LOG]: "Hello, what is your name?" 
[LOG]: "Hey! My name is Fábio." 

或者,如果您想要更通用的东西:

export function wrapFunction<TArgs extends any[], TReturn>(
  targetFunction: (...parameters: TArgs) => TReturn,
  wrapperFunction: (...parameters: TArgs) => void,
): (...parameters: TArgs) => TReturn {
  return (...parameters: TArgs) => {
    wrapperFunction(...parameters);
    return targetFunction(...parameters);
  };
}



// --------------- Example
const someFunction = (name: string) => {
    console.log(`Hey! My name is ${name}.`);
}

const wrapperFunction = (name: string) => {
    console.log(`The wrapper - Hey! My name is ${name}.`);
}

const wrappedFunction = wrapFunction(someFunction, wrapperFunction);
wrappedFunction("Fábio");

输出

[LOG]: "The wrapper - Hey! My name is Fábio." 
[LOG]: "Hey! My name is Fábio."

这是我找到的另一种方法。它甚至保留了包装函数的泛型:

const newFunction = ((one, two) => {
    // something before
    const result = oldFunction(one, two)
    // something after
    return result
}) as typeof oldFunction