microsoft / microsoft/TypeScript
Inconsistent narrowing of adjacently tagged unions
まだ誰も着手していません。
- 主要言語
- 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
💻 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"
}
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
リンク先の TypeScript Playground から始め、typeof、property、user-defined type-predicate のチェックを含めて、nameState1 と nameState2 の例を再現します。結果として得られる narrowing の挙動を期待される型と比較します。同等の union ケースで、関連する state プロパティが一貫して狭められれば完了です。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- typescript
- 領域
- compilers
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 停滞
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 42/100