为什么 TypeScript 在这个辨别联合返回类型上不会报错?

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

Why doesn't Typescript give an error on this discriminating union return type

问题

I'm curious why typescript is not giving an error for the return type of the test1 function, whereas for the test2 function it is.

I would assume typescript is capable of determining the wrong return value for test1 as it can for test2.

interface A {
  status: 'a'
  data: {
    value: string
  }
}

interface B {
  status: 'b'
}

type AB = A | B

function test1(arg: 'foo' | 'bar'): AB {
  // this doesn't give a TS error, where it is actually wrong  
  return {
    status: arg === 'foo' ? 'b' : 'a',
    data: {
      value: 'hello'
    }
  }
}

function test2(arg: 'foo' | 'bar'): AB {
  if(arg === 'foo') {
    return {
      status: 'a',
      data: {
        value: 'hello'
      }
    }
  }
  // This does give an error (which I would expect)
  return {
    status: 'b',
    data: {
      value: 'hello'
    }
  }
}

Link to TS Playground example

Update:
If this is "by design" as @wonderflame mentions in the comment would there be an alternative way where you don't have to explicitly return each type?

英文:

I'm curious why typescript is not giving an error for the return type of the test1 function, whereas for the test2 function it is.

I would assume typescript is capable of determining the wrong return value for test1 as it can for test2.

interface A {
  status: 'a'
  data: {
    value: string
  }
}

interface B {
  status: 'b'
}

type AB = A | B

function test1(arg: 'foo' | 'bar'): AB {
  // this doesn't give a TS error, where it is actually wrong  
  return {
    status: arg === 'foo' ? 'b' : 'a',
    data: {
      value: 'hello'
    }
  }
}

function test2(arg: 'foo' | 'bar'): AB {
  if(arg === 'foo') {
    return {
      status: 'a',
      data: {
        value: 'hello`'
      }
    }
  }
  // This does give an error (which I would expect)
  return {
    status: 'b',
    data: {
      value: 'hello'
    }
  }
}

Link to TS Playground example

Update:
If this is "by design" as @wonderflame mentions in the comment would there be an alternative way where you don't have to explicitly return each type?

答案1

得分: 1

TypeScript 的对象类型是开放可扩展的,而不是封闭精确(使用 Flow 的术语,并按照 microsoft/TypeScript#12936 中的要求)。由于 B 的定义完全没有提到 data 属性,这意味着 B 可能包含或不包含 data 属性。它不要求有一个,但也不禁止有一个。

这被 TypeScript 的某些情况下会执行多余属性检查所复杂化,但该功能更像是一个代码检查警告,旨在防止您设置编译器可能会跟踪丢失的属性。它实际上并不是用来使对象类型封闭或精确的。

多余属性检查通常会对辨别联合产生影响,但在 test1 中将 status 属性本身设为联合类型足以混淆事情,以至于它不会触发。再次强调,您不应该依赖多余属性检查来确保正确性。


如果您希望 B 一直禁止 data 属性,您需要明确更改 B 的定义以表示这一点。嗯,TypeScript 实际上不会让您禁止属性。您能够做到的最接近的方法是将其设为可选属性,并将其类型定义为不可能的 never 类型。这意味着该属性要么缺失,要么存在但没有定义的值(根据您的编译器选项,这可能包括 undefined;请参阅 --exactOptionalPropertyTypes 以获取更多信息)。

像这样:

interface B {
  status: 'b'
  data?: never; // <-- B 上没有 data
}

现在事情开始按您的预期方式运作:

function test1(arg: 'foo' | 'bar'): AB {
  return { // 错误!
    status: arg === 'foo' ? 'b' : 'a',
    data: {
      value: 'hello'
    }
  }
}

代码的 Playground 链接

英文:

TypeScript's object types are open and extendible, not sealed or exact (using Flow terminology, and as requested in microsoft/TypeScript#12936). Since B's definition makes no mention whatsoever of the data property, it means that a B may or may not contain a data property. It doesn't require one, but it doesn't prohibit one either.

This is complicated by the fact that TypeScript will sometimes perform excess property checking on object literals, but that feature is more of a linter warning and is meant to prevent you from setting properties the compiler will probably lose track of. It is not really meant to make object types sealed or exact.

Excess property checking does usually kick in for discriminated unions, but your approach in test1 of making the status property itself a union confused things enough to not have it activate. Again, you're not really supposed to rely on excess property checking for correctness.


If you want B to consistently prohibit a data property, you will need to change B's definition to say that explicitly. Well, TypeScript doesn't actually let you prohibit a property. The closest you can get is to make it an optional property of a type like the impossible never type. This means the property will either be missing, or present without a defined value (depending on your compiler options that could include undefined; see `--exactOptionalPropertyTypes for more info).

Like this:

interface B {
status: &#39;b&#39;
data?: never; // &lt;-- no data on B
}

And now things start behaving the way you expect:

function test1(arg: &#39;foo&#39; | &#39;bar&#39;): AB {
return { // error!
status: arg === &#39;foo&#39; ? &#39;b&#39; : &#39;a&#39;,
data: {
value: &#39;hello&#39;
}
}
}

Playground link to code

huangapple
  • 本文由 发表于 2023年6月27日 21:51:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/76565570.html
匿名

发表评论

匿名网友

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

确定