TypeScript类型守卫问题

huangapple go评论53阅读模式
英文:

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&lt;X1&gt;).v1 !== undefined;
}
function isX2(arg: X1 | X2): arg is X2 {
    return (arg as Partial&lt;X1&gt;).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: &#39;foo&#39;, v1: &#39;bar&#39; } 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 &amp; { v1: undefined } {
    return (arg as Partial&lt;X1&gt;).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.

Playground link

huangapple
  • 本文由 发表于 2023年2月8日 21:25:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/75386459.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定