TL;DR:你并不疯狂,但你对编译器的期望比它所能提供的要多。使用类型断言并继续。
TypeScript 并不倾向于确定类型保护检查的所有可能含义。检查对象的属性会缩小对象本身的类型的少数几个地方之一是当对象是 discriminated union 并且您正在检查的属性是判别属性时。在您的情况下,par 本身甚至不是联合类型,更不用说有区别的联合了。因此,当您检查par.b 时,编译器会缩小par.b,但不会将该缩小向上传播到par 本身的缩小。
它可以做到这一点,但问题是这样的计算对于编译器来说很容易变得昂贵,如this comment by one of the language architects中所述:
似乎对于控制流图中对x 的每一个引用,我们现在都必须检查每个以x 作为基本名称的类型保护。换句话说,为了知道x 的类型,我们必须查看x 属性的所有类型保护。这有可能产生大量工作。
如果编译器像人类一样聪明,它可能只在它们可能有用时才执行这些额外的检查。或者也许一个聪明的人可以写出一些对这个用例来说足够好的启发式算法;但我想在实践中,这并不是任何人进入该语言的优先事项。我还没有找到建议这一点的open issue in GitHub,所以如果你对此有强烈的感觉,你可能想要提交一个。但我不知道它会受到多大的欢迎。
在没有更聪明的编译器的情况下,有一些变通方法:
最简单的解决方法是接受你比编译器更聪明,并使用type assertion 告诉它你确信你正在做的事情是安全的,它不应该太担心验证它。类型断言一般来说有点危险,因为如果你使用了一个断言并且你的断言是错误的,那么你只是对编译器撒了谎,由此产生的任何运行时问题都是你的错。但在这种情况下,我们可以非常自信:
function aAssert(par: {
a: string;
b: null | string;
}): { a: string; b: string } | undefined {
if (par.b === null) {
return;
}
return par as { a: string; b: string }; // I'm smarter than the compiler ?
}
这可能是这里的方法,因为它使您的代码基本相同,并且断言非常温和。
另一种可能的解决方法是使用user-defined type guard 函数来缩小par 本身的范围。这有点棘手,因为不作用于联合类型的类型保护函数不会在“else”分支中缩小范围......可能是因为该语言缺少negated types。也就是说,如果你有一个类型保护function guard(x: A): x is A & B,并调用if (guard(x)) { /*then branch*/ } else { /*else branch*/ },x 将在“then”分支内缩小为A & B,但在“else”中将只是A分支。没有要使用的 A & not B 类型。你能得到的最接近的是if (!guard(x)) {} else {},但这只是切换哪个分支变窄。
所以我们可以这样做:
function propNotNull<T, K extends keyof T>(
t: T,
k: K
): t is { [P in keyof T]: P extends K ? NonNullable<T[P]> : T[P] } {
return t[k] != null;
}
propNotNull(obj, key) 守卫将在返回 true 时将 obj 缩小为已知 obj.key 不是 null(或 undefined... 只是因为 NonNullable<T> 的类型是标准实用程序类型)。
现在你的a() 函数可以写成:
function aUserDefinedTypeGuard(par: {
a: string;
b: null | string;
}): { a: string; b: string } | undefined {
if (!propNotNull(par, "b")) {
return;
} else {
return par;
}
}
检查!propNotNull(par, "b") 导致par 在第一个分支中根本不缩小,但在第二个分支中将par 缩小到{a: string; b: string}。这足以使您的代码编译器没有错误。
但我不知道与类型断言相比是否值得额外的复杂性。
好的,希望对您有所帮助;祝你好运!
Link to code