在 Typescript 中将函数用作 first-class 值时,有没有办法传递通用绑定?

When using a function as a first-class value in Typescript, is there a way to pass Generic bindings?

我正在尝试包装 typescript 函数,return添加额外行为的新函数。例如,在下面的最小重现中,(参见 playground)包装后的函数总是 return 是一个 Promise,其参数和 return 值是控制台记录的。

但是,我发现如果原始函数有任何泛型类型,我不知道如何绑定该泛型类型,因此它在包装函数上可用,即使它在调用范围内是众所周知的.

下面的脚本中的 const delayedValue 演示了该问题。 Typescript 似乎认为这只能是未知的。

有什么办法吗? wrappedDelay 的定义使得通用 V 参数可以通过,从而告知我们可以从 wrappedDelay 函数中得到什么 return 类型。

export type AnyFn = (...args: any[]) => any;

/** If sync function - ReturnType. If async function - unwrap Promise of ReturnType */
export type Result<Fn extends AnyFn> = Fn extends () => Promise<infer Promised>
  ? Promised
  : ReturnType<Fn>;

/** Async function with same params and return as Fn, regardless if Fn is sync or async  */
export type WrappedFn<Fn extends AnyFn> = (...parameters:Parameters<Fn>) => Promise<Result<Fn>>

/** Construct wrapped function from function reference */
function wrapFn<Fn extends AnyFn>(fn:Fn) : WrappedFn<Fn>{
    return async (...parameters:Parameters<Fn>) => {
        console.log(`Parameters are ${parameters}`);
        const result = await fn(...parameters);
        console.log(`Result is ${result}`);
        return result;
    }
}

function sum(a:number, b:number){
    return a + b;
}

function delay<V>(value: V, ms:number){
    return new Promise<V>((resolve, reject) => {
        setTimeout(() => resolve(value), ms)
    })
}

const wrappedSum = wrapFn(sum)

const wrappedDelay = wrapFn(delay)

async function example() {
    const sum = await wrappedSum(3,4)
    const delayedValue = await wrappedDelay("hello", 1000)
}  

TypeScript 不直接支持 microsoft/TypeScript#1213 中要求的那种“高等类型”,因此没有 general 方法以编程方式操作泛型函数一种表达函数类型 F 到函数类型 G 纯类型级别 转换的方法,其中 F 上的任何泛型类型参数都被转移至 G.

幸运的是,自 TypeScript 3.4 起, 支持 higher order type inference from generic functions,您可以在其中为 特定的 函数获得这样的行为在 值级别 ,例如 wrapFn() 作用于通用的输入函数。因此,如果 f 是类型 F 的函数,并且 const g = wrapFn(f),则可以编写 wrapFn() 以便 g 类型为 G ,其中 F 的任何泛型类型参数已转移到 G.

您可以阅读 microsoft/TypeScript#30215 for how this higher order type inference works and the rules you need to follow to get this behavior. In particular, this feature expects that you will have separate type parameters for the function arguments (e.g., A extends any[]) and for the return (e.g., R). It does not work with a generic function type like F extends (...args: any[])=>any where you use conditional 实用程序类型,例如 Parameters<F>ReturnType<F> 以从中提取参数类型或 return 类型。

因此,如果您将 WrappedFnwrapFn 更改为以下内容:

type WrappedFn<A extends any[], R> = (...parameters: A) => Promise<R>

function wrapFn<A extends any[], R>(fn: (...args: A) => R): WrappedFn<A, R> {
    return async (...parameters: A) => {
        console.log(`Parameters are ${parameters}`);
        const result = await fn(...parameters);
        console.log(`Result is ${result}`);
        return result;
    }
}

那么事情就如你所料,至少对于你问题中的示例代码而言:

const wrappedDelayValue = wrapFn(delay);
// const wrappedDelayValue: <V>(value: V, ms: number) => Promise<Promise<V>>

const delayedValue = await wrappedDelayValue("hello", 1000);
// const delayedValue: string    
console.log(delayedValue.toUpperCase()) // no compiler error now

Playground link to code