TypeScript:Map`getOrCreate`helper函数中的不健全问题

TypeScript:Map`getOrCreate`helper函数中的不健全问题,typescript,generic-variance,Typescript,Generic Variance,我有一个助手函数,用于从地图中获取条目,如果它还不存在,则添加它 export function mapGetOrCreate<K, V>(map: Map<K, V>, key: K, valueFn: (key: K) => V): V { let value = map.get(key); if (value === undefined) { value = valueFn(key); assert(value

我有一个助手函数,用于从地图中获取条目,如果它还不存在,则添加它

export function mapGetOrCreate<K, V>(map: Map<K, V>, key: K, valueFn: (key: K) => V): V {
    let value = map.get(key);
    if (value === undefined) {
        value = valueFn(key);
        assert(value !== undefined);
        map.set(key, value);
    }
    return value;
}
它仍然不可靠,但至少TypeScript不能自动推断类型,这是一个小小的改进

问题:

  • 有没有更好的方法来完成我正在做的事情
  • 有没有办法使
    mapGetOrCreate2
    比原来的更糟糕
  • 您正在查找中请求的非重要类型参数用法。没有官方的支持,但是有很多技术可以达到这个效果,其中之一就是你已经在使用的技术


    为了让以后遇到这个问题的人明白,这里的不合理之处在于,每当
    U
    可分配给
    T
    时,TypeScript允许
    Map
    可分配给
    Map

    function technicallyUnsound<K, T, U extends T>(mapU: Map<K, U>) {
        const mapT: Map<K, T> = mapU;
    }
    
    但是,考虑到这一点,强化
    mapGetOrCreate()
    的选项是什么


    使用
    mapGetOrCreate()
    的真正问题是TypeScript的泛型类型参数推断算法。让我们把
    mapGetOrCreate()
    归结为一个函数
    g()
    ,在这个函数中,我们忘记了键类型
    K
    (只使用
    string
    )。在以下通话中:

    declare function g<T>(map: Map<string, T>, valueFn: (key: string) => T): T;
    g(m, createA); // no error, not good
    // function g<A>(map: Map<string, A>, valueFn: (key: string) => A): A
    
    另一种非官方的方式是删除,这“降低了推理站点的优先级”。这不太可靠,仅适用于
    X
    类型,其中
    X&{}
    不会缩小
    X
    (因此
    X
    中不能有
    未定义的
    空的
    ),但它也适用于这种情况:

    type NoInfer<T> = T & {};
    declare function g<T>(map: Map<string, T>, valueFn: (key: string) => NoInfer<T>): T;
    g(m, createA); // error! A is not assignable to type NoInfer<B>
    // function g<B>(map: Map<string, B>, valueFn: (key: string) => NoInfer<B>): B
    g(m, createB); // okay
    g(m, createC); // okay
    
    这三种方法都是权宜之计,并非在所有情况下都有效。如果您对细节和讨论感兴趣,可以阅读ms/TS#14829。我这里的主要观点是,如果您的技术适用于您的用例,那么它可能很好,而且我不知道有任何明显优于您的技术

    我认为修改后的版本比原始版本更糟糕的唯一方法是它更复杂,需要更多的测试。您试图避免的问题在实践中似乎并不经常出现(这就是为什么方法二变是语言的一部分);既然您实际遇到了这个问题,那么您可能应该实际解决它,因此增加的复杂性是值得的。但由于面对不健全的类型系统,这种强化从根本上说是不可能的,因此很快就会出现收益递减点,之后最好还是接受不健全的情况,编写一些更具防御性的运行时检查,并放弃试图开拓一个纯健全的领域

    function technicallyUnsound<K, T, U extends T>(mapU: Map<K, U>, k: K, t: T) {
        mapU.set(k, u); // error, as desired
        const mapT: Map<K, T> = mapU;
        mapT.set(k, t); // no error, uh oh
    }
    
    const m = new Map<string, Date>();
    technicallyUnsound(m, "k", {});
    m.get("k")?.getTime(); // compiles okay, but
    // RUNTIME ERROR: u is not defined
    
    mapGetOrCreate2(m, "", createA) // error, but
    const n: Map<string, A> = m;
    mapGetOrCreate2(n, "", createA) // no error
    
    declare function g<T>(map: Map<string, T>, valueFn: (key: string) => T): T;
    g(m, createA); // no error, not good
    // function g<A>(map: Map<string, A>, valueFn: (key: string) => A): A
    
    declare function g<T, U extends T>(map: Map<string, T>, valueFn: (key: string) => U): T;
    g(m, createA); // error! A is not assignable to B
    // function g<B, B>(map: Map<string, B>, valueFn: (key: string) => B): B
    g(m, createB); // okay
    g(m, createC); // okay
    
    type NoInfer<T> = T & {};
    declare function g<T>(map: Map<string, T>, valueFn: (key: string) => NoInfer<T>): T;
    g(m, createA); // error! A is not assignable to type NoInfer<B>
    // function g<B>(map: Map<string, B>, valueFn: (key: string) => NoInfer<B>): B
    g(m, createB); // okay
    g(m, createC); // okay
    
    type NoInfer<T> = [T][T extends any ? 0 : never];
    declare function g<T>(map: Map<string, T>, valueFn: (key: string) => NoInfer<T>): T;
    g(m, createA); // error! A is not assignable to type B
    // function g<B>(map: Map<string, B>, valueFn: (key: string) => B): B
    g(m, createB); // okay
    g(m, createC); // okay