microsoft / microsoft/TypeScript

Inconsistent narrowing of adjacently tagged unions

オープン
#60,685 コメント 1 件 リアクション 2 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

Experimentation Needed Suggestion
主要言語
Go
スター
111k
フォーク
14.4k
平均マージ
1日 19時間
マージ済み PR(30日)
117

説明

🔎 Search Terms

narrow adjacently tagged union

🕗 Version & Regression Information

This changed between versions v4.8.4 and v4.9.5:

Prior to v4.9.5, TypeScript would work the same both for SomeInterface | string | undefined and SomeInterface | string, which is still undesired behavior, but it is consistent.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.8.0-dev.20241204#code/JYOwLgpgTgZghgYwgAgMpjpAPAFQJ4AOEAfMgN4BQyyAzhpAFzL5EDcFAvhRaJLIigCqNaADk4AWxSVqMYFDpM6UUAHN21ADZw6AfiVgVIdZ24B6M8gDCcEAHIwyAK4hgAexDIQcKFDcB3ZDcYWkM1ZAAfIIAjACsIBDAGbgQPOi9JCHRMCABGZABecg5kHWcQAGsQAM8ymWQANzhNJwgDIxNqOhymbOxlNWJ2Eqj6ppa25GExTI1QnrR6CCxpqHEpIc52HhCACjBCCGCMqT68gDpx1sKCooAiAeM7gEpyKhOspdzz7shaTGANDkEBoixyWEeqk2XAoFmQACEnI5gI5-G4oBVQcAQgBJUoAE3x5XxEDkIAgRLAbmQYAAFigJMAAB7JCipEDpbynJYAJkKxVKoJcVRqgre1CukxcJLJFLmv0mZyw0tJoAp0Mi4sazVa7TU8qWvSWELCxg1o3ekqYq3WEANCyVNsy0O22OQ+0Oxy5nxyPMuOpQt3ukJeWu9Zz9Cv+YEBwNBSsh0O45ks8I8TlBuSYAHkCAQ3DQUSh2fiUe5PID7I52YW6BBwDS3G5WW6PUQvZkzt9JTd7m44gkwHdkAAyEfIO5yBRD5CgD5d-0TV71cNfH5LaOx4AgsHYJ0bYbcVsHdshVe+xfXIMT-vxRLDscTqd0Ydz8+QP2S5fvd8QSMbmgASBbd42NfcSEPWFU3TUEeSYKx6QQCpwgzcIAANATQ5B8TcEEq2QNEMSCTxojcOlynLGgWz2QF0A6XZf27ANnm-ahGPXHJN2AncE1NKFINbWi+IYzteUvCAWLDUSLyjQCY240DwUTSCYBcRJy1nGg6LUXYrVKEA8GeJge0BUIOi1KAIDAJwoE8E8jhCHtrwePi7lMZM4QAQRAIkYHRGl6WQSzUigfEABoAtM0y6RQezkDxDMKUbZBoicYBNEpekRAotJWTigARBI3AkfMRHxFhlgq0gigq5AICZSAfNBCr3l0LVqAlAMmAquYOvmRhd0qw5Nj6rhqCYckGmgVggA

💻 Code
interface State<Type> {
  state: Type;
}

interface UserName {
  first: string;
  last?: string;
}

// Can't union narrow of string | object:

const nameState1 = {} as unknown as {
  value: string;
  state: State<string>;
} | {
  value: UserName;
  state: State<UserName>;
};

if (typeof nameState1.value === "string") {
  nameState1.state satisfies State<string>;
  //               ^^^^^^^^^
  // Type 'State<string> | State<UserName>' does not satisfy the expected type 'State<string>'.
  //   Type 'State<UserName>' is not assignable to type 'State<string>'.
  //     Type 'UserName' is not assignable to type 'string'.(1360)
}

// But it works if I add undefined to the mix:

const nameState2 = {} as unknown as {
  value: undefined;
  state: State<undefined>;
} | {
  value: string;
  state: State<string>;
} | {
  value: UserName;
  state: State<UserName>;
};

if (typeof nameState2.value === "string") {
  nameState2.state satisfies State<string>;
}
🙁 Actual behavior

nameState1 won't narrow down to State<string>, but when undefined is in the union (nameState2) it does narrow down with the same condition.

🙂 Expected behavior

I would expect nameState1 to correctly narrow down to State<string> or at least not to change behavior when adding undefined to the mix.

Additional information about the issue

This inconsistency is not consistent across different narrowing methods too:

// Bonus 1: Opposite condition isn't consistent too:

if (typeof nameState1.value === "object" && "first" in nameState1.value) {
  nameState1.state satisfies State<UserName>;
  //               ^^^^^^^^^
}

if (typeof nameState2.value === "object" && "first" in nameState2.value) {
  // No type error!
  nameState2.state satisfies State<UserName>;
}

// Bonus 2: Checking using `is` doesn't work on both unions:

if (isString(nameState1.value)) {
  nameState1.state satisfies State<string>;
  //               ^^^^^^^^^
}

if (isString(nameState2.value)) {
  nameState2.state satisfies State<string>;
  //               ^^^^^^^^^
}

function isString(value: any): value is string {
  return typeof value === "string"
}

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

リンク先の TypeScript Playground から始め、typeof、property、user-defined type-predicate のチェックを含めて、nameState1 と nameState2 の例を再現します。結果として得られる narrowing の挙動を期待される型と比較します。同等の union ケースで、関連する state プロパティが一貫して狭められれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
typescript
領域
compilers
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
おおむね明確
初心者へのやさしさ
42/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。