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