我认为造成这种情况的原因有很多。
一个问题是当被测试的属性键是一个变量时,编译器不会进行类型保护缩小;请参阅microsoft/TypeScript#10530 了解更多信息。
另一个因素是不能安全地完成对其键类型未知为单例的属性的读取和后续写入。见microsoft/TypeScript#32693。编译器没有意识到property 是两行中完全相同的键。它所理解的只是被访问的属性具有相同的类型,这不够严格,除非类型已经知道是单例。
最后,面对依赖于未指定的泛型类型参数(如T 和K)的值,编译器只是不太擅长控制流分析。规范的问题是microsoft/TypeScript#13995。即使您测试property 是keyof T 的特定子类型,编译器也不会缩小K 参数,请参阅microsoft/TypeScript#24085。充其量它会将property 的类型缩小到某个路口K & ...,这可能有用也可能没有帮助。
在展示变通方法之前,需要在此处提出一个警告:这在技术上并不安全。如果您的主机有一个类型已知为布尔文字 false 的属性,如:
interface Foo {
bar: false
}
declare const foo: Foo;
那么你就会遇到问题:
setTrue(foo, "bar");
foo.bar; // false or true?
您对setTrue() 的实现只是检查foo.bar 是否属于boolean 类型,就是这样。但是现在我们通过将 type false 的属性设置为值 true 来欺骗编译器。所以foo.bar 现在在运行时是true,但在你的IDE 中是false。
我会忽略这个皱纹,但它仍然存在,所以要小心。
所以,解决方法。到目前为止,最简单的方法就是使用 type assertion 并继续:
function setTrue<T, K extends keyof T>(host: T, property: K) {
if (typeof host[property] === 'boolean')
(host as any as Record<K, boolean>)[property] = true;
}
这里我们已经断言,如果host[property] 是boolean 类型,那么我们会将host 视为Record<K, boolean>:在K 类型的键上具有boolean 类型的属性值的对象。
不太直接的方法是将编译器重构为一些较小的部分,编译器可以自行验证,也可以将其制成user-defined type guard,让编译器按照我们希望的方式进行类型分析:
function setTrueKnownBoolean<K extends PropertyKey>(host: Record<K, boolean>, property: K) {
host[property] = true;
}
function isBooleanProperty<K extends keyof T, T>(
host: T, property: K): host is T & Record<K, boolean> {
return typeof host[property] === "boolean";
}
function setTrue2<K extends keyof T, T>(host: T, property: K) {
if (isBooleanProperty(host, property)) {
setTrueKnownBoolean(host, property);
}
}
函数setTrueKnownBoolean 只允许在host 对象上调用,这些对象在property 键处已知具有boolean 属性,因此实现不需要内部类型检查。函数isBooleanProperty 充当类型保护,将host 的类型从T 缩小到T & Record<K, boolean>。现在setTrue2()可以通过组合这两个函数来实现。
Playground link to code