【问题标题】:Typescript function overloads with generic optional parameters带有通用可选参数的打字稿函数重载
【发布时间】:2017-11-09 17:56:14
【问题描述】:

我正在尝试编写一个高阶函数来包装输入函数并缓存最近调用的结果作为副作用。基本函数 (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) =&gt; number。我希望fooWithCache 的签名是(a: number, b?: number) =&gt; number,就像foo。 有没有办法解决这个问题?

(顺便说一句,有没有办法声明重载,这样我就不必重复每个重载的函数类型(...) =&gt; 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 而不是它们解析的值的情况下,断言函数类型可能是安全的。但是,如果没有unimplementedlanguageimprovements,上述同步或异步场景是不可能的。

【问题讨论】:

  • 根据您的代码,这应该是let fooResult2 = fooWithCache(1, 2) 的正确过载?
  • 我希望fooWithCache 的签名是(a: number, b?: number) =&gt; number,就像foo
  • 编辑了问题以澄清我的期望。
  • 当你使用const foo = (x: number, y?: number) =&gt; x + (y || 0);console.log(fooWithCache)的值是多少你试过用const foo = (x: number, y: number = 0) =&gt; x + y ;为第二个参数设置一个默认值。

标签: typescript generics higher-order-functions


【解决方案1】:

通过向下列表选择第一个匹配的重载来选择重载。

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

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 ...
}

无重载,返回值始终与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 :)

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

【讨论】:

  • 感谢详细的解释!如果我想,比如说,能够为每个呼叫设置它们的密钥(例如const fooWithCache = withCache(foo); let barResult = fooWithCache("bar", 1, 2))怎么办?
  • 如果你的意思是获取一个函数类型并为其添加一个参数,那么在 TypeScript 中没有像 variadic kinds 这样的东西,它目前不是语言的一部分。你可以像上面那样尝试做一堆重载,但是你会发现你需要对你可以接受的函数类型做出妥协。或者你可以重构为 TypeScript 可以理解的东西;也许像withCache(foo).forKey("bar")(1,2).
  • 刚刚尝试了您的原始答案,但遇到了一些麻烦。使用我在问题中给出的实现,但是使用您的新签名,Typescript 在内部函数上给了我以下错误:Type '(...args: any[]) =&gt; any' is not assignable to type 'F'. 我错过了什么吗?
  • TypeScript 在实现中无法推断出您返回的内容实际上是 F 类型。你可以使用像return ((...args: any[]) =&gt; {...}) as any as F; 这样的断言,它不会抱怨。这是相当安全的:除了this 或具有额外属性的函数可能会出现怪异之外,您输出的函数类型将与输入的类型相同。
  • 最后一个问题:如果F 是一个异步函数并且我想缓存解析的值,这将如何工作?实现会很简单,我仍然可以断言返回类型,但是用同步函数调用是不安全的。有没有办法防止这种情况发生,或者甚至写一些对两者都有效的东西?我已经用一个例子编辑了这个问题。
猜你喜欢
  • 1970-01-01
  • 2019-11-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多