问题描述
我有一个辅助函数,用于从地图中获取条目,如果它不存在则添加它。
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 在泛型类型变化方面的不健全最终导致了我的这个实际问题:
type A = {a: number};
type B = A & {b: string};
type C = B & {c: boolean};
declare function createA(): A;
declare function createB(): B;
declare function createC(): C;
function f(m: Map<string,B>,element: Base) {
m.set('1',createA()); // TS error (good)
m.set('1',createB());
m.set('1',createC());
mapGetorCreate(m,'1',createA); // missing error!
mapGetorCreate(m,createB);
mapGetorCreate(m,createC);
}
我发现向函数签名添加另一个类型参数 (V2
) 可以“修复”该问题:
export function mapGetorCreate2<K,V,V2 extends V>(map: Map<K,valueFn: (key: K) => V2): V {
let value = map.get(key);
if (value === undefined) {
value = valueFn(key);
assert(value !== undefined);
map.set(key,value);
}
return value;
}
还是不行,但至少TypeScript不能自动推断类型,这是一个小小的改进。
问题:
解决方法
您正在寻找microsoft/TypeScript#14829 中要求的非推论类型参数用法。对此没有官方支持,但是有很多技术可以实现这种效果,其中之一就是您已经在使用的技术。
为了让以后遇到这个问题的任何人都清楚,这里的不健全之处在于,每当 Map<K,U>
可分配给 {{1} 时,TypeScript 都允许将 Map<K,T>
分配给 U
}:
T
这实际上是完全合理的,只要您只是读取地图,但是当您写入给它们时,您最终可能会遇到麻烦: >
function technicallyUnsound<K,T,U extends T>(mapU: Map<K,U>) {
const mapT: Map<K,T> = mapU;
}
这就是 TypeScript 的方式;它具有一组有用的功能,例如对象可变性、子类型化、别名和 method bivariance,可以提高开发人员的生产力,但可以以不安全的方式使用。无论如何,请参阅 this SO answer 以了解有关此类健全性问题的更多详细信息。
真的没有办法完全阻止这种情况;无论您做什么,即使您可以强化 function technicallyUnsound<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
,您也始终可以使用这种别名来绕过它:
mapGetOrCreate()
不过,考虑到这一点,强化 mapGetOrCreate2(m,"",createA) // error,but
const n: Map<string,A> = m;
mapGetOrCreate2(n,createA) // no error
的选项有哪些?
您在使用 mapGetOrCreate()
时遇到的真正问题是 TypeScript 的通用类型参数推断算法。让我们将 mapGetOrCreate()
归结为函数 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
应该由类型T
指定,因为A
返回一个valueFn
,一个A
也是被视为有效的 Map<string,B>
。
理想情况下,您希望编译器仅从 Map<string,A>
推断 T
,然后检查 map
是否可分配给 valueFn
的 (key: string) => T
.在 T
中,您只想使用 valueFn
而不是推断它。
换句话说,您正在寻找非推论类型参数用法,正如 microsoft/TypeScript#14829 中所要求的那样。正如我在开头所说的,没有正式的方法可以做到这一点。
让我们看看非官方的方式:
一种非官方的方式是 use an additional type parameter T
,即 constrained 到原始类型参数 U
。由于 T
将与 U
分开推断,因此从 T
的角度来看,它看起来是“非推断性的”。这就是您使用 T
所做的:
mapGetOrCreate2
另一种非官方方式是 intersect the "non-inferential" locations with {}
,它“降低了该推理站点的优先级”。这不太可靠,仅适用于 declare function g<T,U extends T>(map: Map<string,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
不缩小 X
的类型 X & {}
(因此 X
不能包含 X
或 undefined
),但它也适用于这种情况:
null
我所知道的最后一种方法是利用 conditional types 的求值是 deferred for as-yet unresolved type parameters 这一事实。所以 type NoInfer<T> = T & {};
declare function g<T>(map: Map<string,valueFn: (key: string) => NoInfer<T>): T;
g(m,createA); // error! A is not assignable to type NoInfer<B>
// function g<B>(map: Map<string,valueFn: (key: string) => NoInfer<B>): B
g(m,createC); // okay
将最终评估为 NoInfer<T>
,但这会在类型推断发生后发生。它也适用于此:
T
所有这三种方法都是变通方法,并不适用于所有情况。如果你对它的细节和讨论感兴趣,你可以通读 ms/TS#14829。我在这里的主要观点是,如果您的技术适用于您的用例,那么它可能没问题,而且我不知道有任何明显优越的技术。
我认为修改版比原版更糟糕的唯一方式是它更复杂,需要更多测试。您试图避免的问题实际上在实践中似乎并不经常出现(这就是方法二元性是语言的一部分的原因);既然您实际上遇到了问题,那么您可能应该真正解决它,因此增加的复杂性是值得的。但是,由于面对不健全的类型系统,这种强化从根本上是不可能的,因此很快就会出现收益递减点,之后最好只接受不健全并编写一些更具防御性的运行时检查,并放弃尝试开拓一个纯粹健全的领域。