您正在寻找microsoft/TypeScript#14829中要求的非推理类型参数用法。对此没有官方支持,但有许多技术可以实现这种效果,其中之一就是您已经在使用的技术。
为了让以后遇到这个问题的人清楚,这里的不合理之处在于 TypeScript 允许Map<K, U>
在可分配给Map<K, T>
时U
分配给T
:
function technicallyUnsound<K, T, U extends T>(mapU: Map<K, U>) {
const mapT: Map<K, T> = mapU;
}
这实际上是完全合理的,只要你只是从地图中读取,但是当你给它们写信时,你最终会遇到麻烦:
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
这正是 TypeScript 的方式;它具有一组有用的功能,例如对象可变性、子类型化、别名和方法双变量,可以提高开发人员的工作效率,但可以以不安全的方式使用。无论如何,有关此类健全性问题的更多详细信息,请参阅此 SO 答案。
真的没有办法完全防止这种情况。不管你做什么,即使你可以 harden mapGetOrCreate()
,你总是可以使用这样的别名来解决它:
mapGetOrCreate2(m, "", createA) // error, but
const n: Map<string, A> = m;
mapGetOrCreate2(n, "", createA) // no error
但是,考虑到这一点,强化的选择是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
编译器推断类型参数T
应该由类型指定A
,因为valueFn
返回 an A
,并且 aMap<string, B>
也被认为是有效的Map<string, A>
。
理想情况下,您希望编译器单独推断T
,map
然后检查valueFn
是否可分配(key: string) => T
给T
. 在valueFn
你只想使用 T
而不是推断它。
换句话说,您正在寻找非推理类型参数用法,如microsoft/TypeScript#14829中所要求的那样。正如我在一开始所说的那样,没有官方的方法可以做到这一点。
我们来看看非官方的方式:
一种非官方的方式是使用一个附加类型参数 U
,该类型参数被限制为原始类型参数T
。由于U
将与 分开推断T
,因此从 的角度来看,它将看起来“非推断” T
。这就是你所做的mapGetOrCreate2
:
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
另一种非官方的方法是将“非推理”位置与 相交{}
,这会“降低该推理站点的优先级”。这不太可靠,仅适用于不窄的类型X
(X & {}
因此X
不能X
有undefined
或null
在其中),但它也适用于这种情况:
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
我知道的最后一种方法是使用条件类型的评估被推迟到尚未解决的类型参数这一事实。所以最终NoInfer<T>
将评估为,但这将在类型推断发生之后发生。它也在这里工作:T
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
所有这三种方法都是变通方法,并非在所有情况下都有效。如果您对它的详细信息和讨论感兴趣,可以阅读 ms/TS#14829。我的主要观点是,如果您的技术适用于您的用例,那么它可能没问题,我不知道有任何明显优越的技术。
我想说修改版本比原始版本更糟糕的唯一方法是它更复杂并且需要更多测试。您试图避免的问题实际上在实践中似乎并不经常出现(这就是为什么方法双变量是语言的一部分);由于您实际上遇到了问题,那么您可能应该实际解决它,因此增加的复杂性是值得的。但是由于这种硬化在面对不健全的类型系统时根本不可能,很快就会出现收益递减点,之后最好只是接受不健全并编写一些更具防御性的运行时检查,而放弃尝试开拓一片纯洁的领土。
Playground 代码链接