英文:
TypeScript type guard issue
问题
考虑以下代码:
interface XBase {
a: string;
}
interface X1 extends XBase {
v1: string;
}
interface X2 extends XBase {
v2: string; // A
}
function isX1(arg: X1 | X2): arg is X1 {
return (arg as Partial<X1>).v1 !== undefined;
}
function isX2(arg: X1 | X2): arg is X2 {
return (arg as Partial<X1>).v1 === undefined;
}
function f(x: X1 | X2) {
const x1a = isX1(x) ? x : undefined;
const x1b = isX2(x) ? undefined : x;
if (x1a) { console.log(x1a.v1); }
if (x1b) { console.log(x1b.v1); }
}
这个代码的行为如预期。
当删除标有A
的行时,x1b
的类型变为 undefined
。
为什么?
使用 tsc 版本 4.9.5
英文:
Consider following code:
interface XBase {
a: string;
}
interface X1 extends XBase {
v1: string;
}
interface X2 extends XBase {
v2: string; // A
}
function isX1(arg: X1 | X2): arg is X1 {
return (arg as Partial<X1>).v1 !== undefined;
}
function isX2(arg: X1 | X2): arg is X2 {
return (arg as Partial<X1>).v1 === undefined;
}
function f(x: X1 | X2) {
const x1a = isX1(x) ? x : undefined;
const x1b = isX2(x) ? undefined : x;
if (x1a) { console.log(x1a.v1); }
if (x1b) { console.log(x1b.v1); }
}
This works as expected.
When the line marked A
is removed, the type of x1b
changes to undefined
.
Why?
Using tsc version 4.9.5
答案1
得分: 1
> 当标记为A的行被移除时,x1b的类型会变成未定义。
> 为什么?
当你移除那行时,所有的X1
也都成为了X2
。请记住,对象可以有多余的属性,所以一个X2
必须有a: string
,但也可以有其他任意属性,包括一个字符串类型的v1
。因此,{ a: 'foo', v1: 'bar' }
既是X2
,也是X1
(还是XBase
)。
由于这个原因,TypeScript 推断isX2
永远不可能返回false。如果你传入一个X2
,显然你的函数会返回它是X2
。但如果你传入一个X1
,在类型上来说,它必须是X2
,因此TypeScript 也期望你的函数会返回true。由于它总是返回true,x1b
必须始终是未定义。
现在,你实际的isX2
实现不会让V1
通过,但类型系统说它会。这有点糟糕,但你可以更改isX2
的返回类型,以指定该值不仅是X2
,还不具有v1
属性。
function isX2(arg: X1 | X2): arg is X2 & { v1: undefined } {
return (arg as Partial<X1>).v1 === undefined;
}
通过这种修改,不再保证X1
会返回true。
实际上,这种情况不经常出现。大多数情况下,如果你编写一个X2
类型,那是因为你想要添加一些区分特性,就像你在注释掉A行之前所做的那样。而且只有在没有这些区分特性时才会出现这种情况。
英文:
> When the line marked A is removed, the type of x1b changes to undefined.
>
> Why?
>
When you remove that line, everything that is an X1
is also an X2
. Remember that an object is allowed to have excess properties, so something that's an X2
must have a: string
, but it could also have arbitrary other properties, including a v1
which is a string. Thus, { a: 'foo', v1: 'bar' }
is an X2
as well as an X1
(as well as an XBase
)
Because of this, typescript deduces that it's impossible for isX2
to return false. If you pass in an X2
, then obviously your function will return that it's an X2
. But if you pass in an X1
then as far as the types are concerned, it must be an X2
, and so typescript expects your function will return true as well. Since it always returns true, x1b
must always be undefined.
Now your actual implementation of isX2
wouldn't let a V1
slip through, but the types say it would. It's a bit ugly but you could change your return type on isX2
to specify that not only is the value an X2
, but it doesn't have a v1
property.
function isX2(arg: X1 | X2): arg is X2 & { v1: undefined } {
return (arg as Partial<X1>).v1 === undefined;
}
With that modification, it's no longer guaranteed that an X1
would return true.
In practice this type of situation doesn't come up much. Most of the time, if you're writing an X2 type it's because you want to add some distinguishing features to it, like you had before commenting out line A
. And this will only come up when you don't have those distinguishing features.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论