我希望能够编写一个类型安全的Memoize函数,该函数支持采用泛型函数,并支持通过返回函数的赋值属性清除内部缓存,如下所示:

const memoizeSafeButLosesGenerics = <Args extends unknown[], RV>(fn: (...args: Args) => RV) => {
    const cache = new Map<string, RV>();
    const memoed = (...args: Args): RV => {
        // implementation here...
        
        return fn(...args);
    };
    memoed.clear = () => cache.clear();
    return memoed;
};

不幸的是,当我测试这一点时,TypeScrip不能保留所提供的fn:

const test1 = memoizeSafeButLosesGenerics(<T,>(t: T) => t);
// test1 is:
// {
//     (t: unknown): unknown;
//     clear(): void;
// }

值得注意的是,删除倒数第二行(其中赋值memoed.clear)does将保留泛型类型参数,因此我认为赋值的某些方面会改变memoed的类型,因为它与其推断相关.

有一种解决方法可以保留泛型类型参数,但是它需要类型断言,并允许人们在Memoize函数的实现中做不安全的事情.例如:

const memoizeUnsafePreservesGenericsWorkaround = <Fn extends (...args: unknown[]) => unknown>(fn: Fn) => {
    type RV = ReturnType<Fn>;
    const cache = new Map<string, RV>();
    type UnsafeExampleArgs = Parameters<Fn> | [] | null[];
    const memoed = ((...args: UnsafeExampleArgs): RV => {
        // implementation here...

        // I would expect UnsafeExampleArgs to _not_ be valid arguments to fn
        return fn(...args); // I would expect calling fn _would_ be a valid RV
    }) as Fn;
    const memoedClearable = Object.assign(memoed, {
        clear: () => cache.clear()
    });
    return memoedClearable;
};

const test2 = memoizeUnsafePreservesGenericsWorkaround(<T,>(t: T) => t);
// test2 is ✅:
// (<T>(t: T) => T) & {
//     clear: () => void;
// }

Playground

有没有第三种方法来编写Memoize,在这种方式下,提供的函数的泛型类型参数被保留,在Memoize的实现中是否not涉及类型断言?

推荐答案

目前还没有办法键入memoize(),这样调用者和实现都可以看到您想要的行为. 你必须 Select 一个或另一个.


您的memoizeSafeButLosesGenerics()正在try 使用microsoft/TypeScript#30215中实现的support for higher order type inference from generic functions,但不幸的是,这是非常脆弱的支持,只有当输出直接是另一个没有其他 struct 的函数类型时才起作用.因此,返回(...a: A) => R就可以了,如果传入一个函数,就会得到generic函数.但是,如果您try 将属性添加到返回类型(例如,{ (...a: A): R; x: string })或将函数放入某个容器类型(例如,{ func: (...a: A) => R })或几乎任何其他修改中,则更高阶的推断将失败,并且您传递的任何泛型函数都将获得其类型参数erased,并替换为它们的constraints.您可以阅读MICROSOFT/TYPESCRIPT#30215来查看推理工作的确切条件.这是相当有限的.


您在memoizeUnsafePreservesGenericsWorkaround()中的变通方法只是直接复制整个输入函数类型,这将保留所有内容,包括泛型类型参数.不幸的是,这从定义上来说是不安全的,因为您真的不能保证手动构建的函数将是给定的generic类型constrained到函数类型,即使它们具有相同的输入和输出类型.例如,您的输入函数本身可能具有更多属性,例如

const f = memoizeUnsafePreservesGenericsWorkaround(
  Object.assign(() => { }, { x: 0 })
);
/* const f: (() => void) & {
    x: number;
} & {
    clear: () => void;
}*/

f.x.toFixed(2) // RUNTIME ERROR!

And then memoizeUnsafePreservesGenericsWorkaround claims that the output has an x property, which it doesn't. You can try to patch holes like this in the implementation, if you are likely to actually see them in practice (you probably won't), but the compiler is hopelessly unable to see what you're doing as safe and you'll end up needing type assertions, which is what you're trying to avoid. It doesn't even really try, and will usually just refuse to accept any value as being assignable to a generic type unless that value is already known to be of the exact type. It's the same general situation as in Why can't I return a generic 'T' to satisfy a Partial<T>?.

此外,有时在泛型函数中,编译器会将类型参数加宽到其约束,从而允许不安全的事情发生.除了几个简单易懂的操作之外,执行任何操作的泛型函数实现几乎总是需要小心,并说服自己相信代码将会工作,因为类型判断器并不像您希望的那样有用.


所以,这就对了.就我个人而言,我建议找到您认为对调用者最好的行为,然后只处理实现者的缺点.函数通常实现一次并调用多次,因此任何需要猜测类型判断器的负担可能更好地放在实现者身上,而不是所有可能的调用者身上.

Playground link to code

Typescript相关问答推荐

如何访问Content UI的DatePicker/中的sx props of year picker?'<>

在动态对话框中使用STEP组件实现布线

在类型脚本中的泛型类上扩展的可选参数

如何使用WebStorm调试NextJS的TypeScrip服务器端代码?

参数属性类型定义的REACT-RUTER-DOM中的加载器属性错误

使用2个派生类作为WeakMap的键

我在Angular 17中的environment.ts文件中得到了一个属性端点错误

17个Angular 的延迟观察.如何在模板中转义@符号?

try 使Angular依赖注入工作

TS2739将接口类型变量赋值给具体变量

如果判断空值,则基本类型Gaurd不起作用

Angular NG8001错误.组件只能在一个页面上工作,而不是在她的页面上工作

无法缩小深度嵌套对象props 的范围

如何在TypeScript类成员函数中使用筛选后的keyof this?

为什么我会得到一个;并不是所有的代码路径都返回一个值0;switch 中的所有情况何时返回或抛出?(来自vsCode/TypeScript)

如何避免TS2322;类型any不可分配给类型never;使用索引访问时

递归JSON类型脚本不接受 node 值中的变量

两个子组件共享控制权

当受其他参数限制时,通用参数类型不会缩小

创建通过删除参数来转换函数的对象文字的类型