这是 TypeScript 的两个次要设计限制和一个主要设计限制的组合,您最好重构或使用 type assertion 继续前进。
首先是microsoft/TypeScript#30506。通常,检查对象的一个属性会缩小该属性的表观类型,但不会缩小对象本身的表观类型。唯一的例外是对象是discriminated union 类型并且您正在检查它的判别属性。在您的情况下,A 不是受歧视的工会(它根本不是工会),所以这不会发生。观察:
type A = {
a: string;
b?: string;
}
declare const a: A;
if (a.b) {
a.b.toUpperCase(); // okay
const doesNotNarrowParentObject: { b: string } = a; // error
}
microsoft/TypeScript#42384 有一个更新的打开请求来解决此限制。但就目前而言,无论如何,当您将a.b 检查传播到b 时,这会阻止您的a.b 检查对观察到的a 类型产生任何影响。
您可以编写自己的自定义type guard function 来检查a.b 并缩小a 的类型:
function isBString(a: A): a is { a: string, b: string } {
return !!a.b;
}
if (isBString(a)) {
a.b.toUpperCase(); // okay
const alsoOkay: { b: string } = a; // okay now
}
下一个问题是编译器看不到一个对象,其属性是一个联合,等同于一个联合对象:
type EquivalentA =
{ a: string, b: string } |
{ a: string, b?: undefined }
var a: A;
var a: EquivalentA; // error!
// Subsequent variable declarations must have the same type.
如果编译器将a 视为“具有string 值的b,或 具有undefined b 的东西”,则任何类型的缩小行为都会依靠这种对等。感谢smarter union type checking support introduced in TS 3.5,编译器在某些具体情况下确实理解这种等价性,但它不会发生在类型级别。
即使我们将A 更改为EquivalentA 并将a.b 检查更改为isBString(a),您仍然会遇到错误。
const stillBadB: B = {
...a,
a: 1,
...isBString(a) && { b: Number(a.b) }
} // error!
这就是大问题:control flow analysis 的基本限制。
编译器会检查某些常用的句法结构,并尝试根据这些来缩小明显的值类型。这适用于if 语句之类的结构,或|| 或&& 之类的逻辑运算符。但这些缩小的范围是有限的。对于if 语句,这将是真/假代码块,而对于逻辑运算符,这是运算符右侧的表达式。一旦你离开了这些范围,所有的控制流变窄都被遗忘了。
您不能将控制流缩小的结果“记录”到变量或其他表达式中并在以后使用它们。只是没有机制允许这种情况发生。 (请参阅 microsoft/TypeScript#12184 以获取允许此操作的建议;它被标记为“重新访问” TS4.4 更新,此问题已由 a new control flow analysis feature 修复,但此修复对当前代码,所以我不会进入它)。请参阅 microsoft/TypeScript#37224,它要求在新的对象文字上对此提供支持。
看来你期待的代码
const b: B = {
...a,
a: 1,
...isBString(a) && { b: Number(a.b) }
}
工作,因为编译器应该执行如下分析:
-
a 的类型是{ a: string, b: string } | {a: string, b?: undefined}。
- 如果
a 是{a: string, b: string},那么(除非"" 值有任何奇怪之处),{...a, a: 1, ...isBString(a) && {b: Number(a.b) } 将是{a: number, b: number}。
- 如果
a是{a: string, b?: undefined},那么``{...a, a: 1, ...isBString(a) && {b: Number(ab) }will be a{a: number, b ?: 未定义}`
- 因此,此表达式是一个联合
{a: number, b: number} | {a: number, b?: undefined},可分配给 B。
但这不会发生。编译器不会多次查看同一个代码块,想象某个值已被依次缩小到每个可能的联合成员,然后将结果收集到一个新的联合中。也就是说,它不执行我所说的分布式控制流分析;见microsoft/TypeScript#25051。
这几乎肯定不会自动发生,因为编译器要模拟联合类型的每个值在任何地方都具有所有可能的变窄,这将是非常昂贵的。您甚至不能要求编译器明确地执行此操作(这就是 microsoft/TypeScript#25051 的目的)。
让控制流分析多次发生的唯一方法是给它多个代码块:
const b: B = isBString(a) ? {
...a,
a: 1,
...true && { b: Number(a.b) }
} : {
...a,
a: 1,
// ...false && { b: Number(a.b) } // comment this out
// because the compiler knows it's bogus
}
在这一点上,这真的太丑陋并且与您的原始代码相去甚远,以至于不可信。
正如另一个答案所述,您可以完全使用不同的工作流程。或者你可以在某处使用类型断言来让编译器满意。例如:
const b: B = {
...(a as Omit<A, "b">),
a: 1,
...a.b && { b: Number(a.b) }
} // okay
在这里,当我们将 a 扩展到新的对象字面量中时,我们要求编译器假装它甚至没有 b 属性。现在编译器甚至不考虑生成的b 可能是string 类型的可能性,并且编译没有错误。
甚至更简单:
const b = {
...a,
a: 1,
...a.b && { b: Number(a.b) }
} as B
在这种情况下,如果编译器无法验证您确定它是安全的东西的类型安全性,那么类型断言是合理的。这会将此类安全性的责任从编译器转移到您身上,所以要小心。
Playground link to code