打字稿中的各种变体?

Ami*_*sta 1 typescript

我希望我的函数memoize(fn)返回与类型相同的另一个函数fn

我有一个如下的丑陋解决方案:
编写可变参数泛型的正确方法是什么?

export const memoize = <FN>(fn: FN) : FN => {
  const cache = { };
  const run : any = (...args) => {
    const key = JSON.stringify(args);
    if(!cache[key]) {
      cache[key] = (fn as any)(...args).catch(up => {
        delete cache[key];
        throw up;
      });
    }
    return cache[key];
  };
  return run as FN;
}

const get = memoize((url: string) => fetch(url, {method: 'GET'}));
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 5

不幸的是,Type Script 当前不支持可变参数类型。幸运的是,您可能不需要它。

函数有两个方面:“外部”是调用者使用的签名,而“内部”是实现。理想情况下,您希望签名将外部调用者严格限制在该函数的安全使用范围内,而同时您希望TypeScript确保该函数的内部实现也是安全的。让我们首先看一下外部:

似乎您想memoize使用任意数量的任何类型的参数的函数,该函数返回Promise(对吗?),并且希望它返回相同类型的函数。您现有的签名<FN>(fn: FN) : FN获得“返回相同类型”部分,但不会执行其他任何操作。因此,例如,没有什么可以阻止呼叫者执行此操作:

const bad = memoize((x: string)=>x+"!"); // runtime explosion, no .catch()
const veryBad = memoize("whoops"); // runtime explosion, not a function
Run Code Online (Sandbox Code Playgroud)

这是一个签名,仅允许输入正确的功能:

export const memoize = <FN extends (...args: any[]) => Promise<{}>>(fn: FN): FN => { 
  // ... same implementation 
}
Run Code Online (Sandbox Code Playgroud)

现在,来电者将很高兴:

const get = memoize((url: string) => fetch(url, { method: 'GET' })); // okay
const getBody = memoize((url: string, body: any) => fetch(url, { method: 'GET', body: body })); // okay
const bad = memoize((x: string) => (x + "!")); // error: string is not a Promise
const veryBad = memoize("whoops"); // error: "whoops" is not a function
Run Code Online (Sandbox Code Playgroud)

剩下的就是内部:实现安全。现在,您依赖于断言和断言any(其中隐含了一些内容any)。现在TypeScript知道会fn返回a Promise,您可以放宽其中一些断言了:

export const memoize = <FN extends (...args: any[]) => Promise<{}>>(fn: FN): FN => {
  const cache: { [k: string]: Promise<{}> } = {}; // holds promises
  const run = (...args: any[]) => {
    const key = JSON.stringify(args);
    if (!cache[key]) {
      // fn doesn't have to be any to typecheck
      cache[key] = fn(...args).catch(up => { 
        delete cache[key];
        throw up;
      });
    }
    return cache[key];
  };
  return run as FN;
}
Run Code Online (Sandbox Code Playgroud)

您仍然需要断言那run是type FN,因为TypeScript知道的只是那FN是type的子类型run,而不是那 type run。这样做有一个很好的理由:您可以传入具有额外属性的函数,并且做出了毫无根据的断言,即您还将返回额外属性:

const crazyFunction = Object.assign((url: string) => fetch(url, { method: 'GET' }), { color: 'purple' });
crazyFunction('blah');
console.log(typeof crazyFunction.color); // string
const whoops = memoize(crazyFunction);
console.log(typeof whoops.color); //TS says string, but is undefined!!
Run Code Online (Sandbox Code Playgroud)

我猜你不在乎谁在打电话前开始对他们的功能做奇怪的事情会发生什么memoize; 特别是因为那个人可能是你,而你知道你不会那样做。因此,这对您可能已经足够了。


如果您真的想使实现和调用签名真正安全,那么您可能会发现需要各种可变字体,而TypeScript没有。您可以通过接受最多一些但数量有限的参数(例如9)的函数来伪造它:

type Func<R, A1, A2, A3, A4, A5, A6, A7, A8, A9> = (a1: A1, a2?: A2, a3?: A3, a4?: A4, a5?: A5, a6?: A6, a7?: A7, a8?: A8, a9?: A9) => R;
export const memoize = <R, A1=never, A2=never, A3=never, A4=never, A5=never, A6=never, A7=never, A8=never, A9=never>(fn: Func<Promise<R>, A1, A2, A3, A4, A5, A6, A7, A8, A9>): Func<Promise<R>, A1, A2, A3, A4, A5, A6, A7, A8, A9> => {
  const cache: { [k: string]: Promise<R> } = {};
  const run : Func<Promise<R>, A1, A2, A3, A4, A5, A6, A7, A8, A9> = (a1,a2,a3,a4,a5,a6,a7,a8,a9) => {
    const key = JSON.stringify([a1,a2,a3,a4,a5,a6,a7,a8,a9]);
    if (!cache[key]) {
      cache[key] = fn(a1,a2,a3,a4,a5,a6,a7,a8,a9).catch(up => {
        delete cache[key];
        throw up;
      });
    }
    return cache[key];
  };
  return run;
}
const get = memoize((url: string) => fetch(url, { method: 'GET' })); // okay
get('hello') // okay
get('hello', 2); // error, 2 is not assignable to undefined
Run Code Online (Sandbox Code Playgroud)

但这可能对您来说太过分了。


希望能有所帮助。祝好运!