带有通用可选参数的 Typescript 函数重载

Typescript function overloads with generic optional parameters

我正在尝试编写一个高阶函数来包装输入函数并将最近调用的结果缓存为副作用。基本函数 (withCache) 看起来像这样:

function cache(key: string, value: any) {
    //Some caching logic goes here
}

function withCache<R>(key: string, fn: (...args: any[]) => R): (...args: any[]) => R {
    return (...args) => {
        const res = fn(...args);
        cache(key, res);
        return res;
    }
}

const foo = (x: number, y: number) => x + y;
const fooWithCache = withCache("foo", foo);
let fooResult1 = fooWithCache(1, 2); // allowed :)
let fooResult2 = fooWithCache(1, 2, 3, 4, 5, 6) // also allowed :(

现在我知道我可以使这种类型安全 - 在一定程度上 - 使用函数重载,如下所示:

function withCache<R>(key: string, fn: () => R): () => R
function withCache<R, T1>(key: string, fn: (a: T1) => R): (a: T1) => R
function withCache<R, T1, T2>(key: string, fn: (a: T1, b: T2) => R): (a: T1, b: T2) => R
function withCache<R>(key: string, fn: (...args: any[]) => R): (...args: any[]) => R {
    // implementation ...
}

const foo = (x: number, y: number) => x + y;
const fooWithCache = withCache("foo", foo);
let fooResult1 = fooWithCache(1, 2); // allowed :)
let fooResult2 = fooWithCache(1, 2, 3, 4, 5, 6) // not allowed :)

当我尝试允许带有可选参数的函数时出现问题(最后一个重载是新的):

function withCache<R>(key: string, fn: () => R): () => R
function withCache<R, T1>(key: string, fn: (a: T1) => R): (a: T1) => R
function withCache<R, T1, T2>(key: string, fn: (a: T1, b: T2) => R): (a: T1, b: T2) => R
function withCache<R, T1, T2>(key: string, fn: (a: T1, b?: T2) => R): (a: T1, b?: T2) => R
function withCache<R>(key: string, fn: (...args: any[]) => R): (...args: any[]) => R {
    // implementation ...
}

const foo = (x: number, y?: number) => x + (y || 0);
const fooWithCache = withCache("foo", foo);
let fooResult1 = fooWithCache(1); // allowed :)
let fooResult2 = fooWithCache(1, 2) // not allowed, but should be :(

问题似乎是 Typescript 为 withCache 选择了错误的重载,结果是 fooWithCache 的签名是 (a: number) => number。我希望 fooWithCache 的签名是 (a: number, b?: number) => number,就像 foo 一样。 有什么办法可以解决这个问题吗?

(附带说明一下,有没有什么方法可以声明重载,这样我就不必重复每个重载的函数类型 (...) => R?)

编辑:

想出了我关于不重复函数类型的第二个问题:只需定义它!

type Function1<T1, R> = (a: T1) => R;
// ...
function withCache<T1, R>(fn: Function1<T1, R>): Function1<T1, R>;

编辑:

这对于异步函数如何工作(假设您想要缓存结果而不是 Promise 本身)?你当然可以这样做:

function withCache<F extends Function>(fn: F) {
  return (key: string) =>
      ((...args) => 
        //Wrap in a Promise so we can handle sync or async
        Promise.resolve(fn(...args)).then(res => { cache(key, res); return res; })
    ) as any as F; //Really want F or (...args) => Promise<returntypeof F>
}

但是与同步函数一起使用是不安全的:

//Async function
const bar = (x: number) => Promise.resolve({ x });
let barRes = withCache(bar)("bar")(1).x; //Not allowed :)

//Sync function
const foo = (x: number) => ({ x });
let fooRes = withCache(foo)("bar")(1).x; //Allowed, because TS thinks fooRes is an object :(

有没有办法防止这种情况?或者编写一个对两者都安全工作的函数?

总结:@jcalz 的回答是正确的。在可以采用同步函数的情况下,或者可以直接使用 Promises 而不是它们解析的值的情况下,断言函数类型可能是安全的。但是,如果没有一些 unimplemented language improvements.

,上述同步或异步场景是不可能的

通过在列表中向下移动并选择第一个匹配的来选择重载。

检查以下成功编译的代码:

declare let f: (a: any, b?: any) => void;
declare let g: (a: any) => void;
g = f; // okay

函数f是一个接受一个或两个参数的函数,而g被声明为一个接受一个参数的函数。您可以将值 f 赋给变量 g,因为您可以在任何可以调用一个参数的函数的地方调用一个或两个参数的任何函数。正是第二个参数是 optional 的事实使这个赋值起作用。

您还可以做其他作业:

f = g; //okay

因为您可以在任何可以调用一个或两个参数的函数的地方调用一个参数的任何函数。这意味着这两个函数类型是可以相互赋值的(尽管它们并不等价,这有点不合理)。


如果我们只看这两个重载:

function withCache<R, T1>(key: string, fn: (a: T1) => R): (a: T1) => R
function withCache<R, T1, T2>(key: string, fn: (a: T1, b?: T2) => R): (a: T1, b?: T2) => R

上面对 fg 的讨论意味着任何与其中一个重载相匹配的东西也将与另一个重载相匹配。因此,将选择您首先列出的那个。抱歉,您实际上不能同时使用它们。

在这一点上,我可以建议你开始想出一套折衷的重载来提供合理的行为,但让我们备份一下:


难道你不想要 withCache() 的类型安全版本吗?这个怎么样:

function withCache<F extends Function>(key: string, fn: F): F {     
    // implementation ...
}

无重载,return 值始终与 fn 参数的类型相同:

const foo = (x: number, y?: number) => x;
const fooWithCache = withCache("foo", foo); // (x: number, y?: number) => number
let fooResult1 = fooWithCache(1); // allowed :)
let fooResult2 = fooWithCache(1, 2) // allowed :)

这对你有用吗?祝你好运!