From 6f6ab2e1594891e207cb401ca58db5439b90c33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 12 Aug 2025 00:18:59 +0200 Subject: [PATCH] Discard types that reduce to `never` before discriminating by discriminable items --- src/compiler/checker.ts | 2 +- ...extuallyTypedByDiscriminableUnion2.symbols | 122 ++++++++++++++++ ...ntextuallyTypedByDiscriminableUnion2.types | 136 ++++++++++++++++++ .../contextuallyTypedByDiscriminableUnion2.ts | 51 +++++++ 4 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/contextuallyTypedByDiscriminableUnion2.symbols create mode 100644 tests/baselines/reference/contextuallyTypedByDiscriminableUnion2.types create mode 100644 tests/cases/compiler/contextuallyTypedByDiscriminableUnion2.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 75be36474222f..cd3fe2db81a65 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -24855,7 +24855,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: (readonly [() => Type, __String])[], related: (source: Type, target: Type) => boolean | Ternary) { const types = target.types; - const include: Ternary[] = types.map(t => t.flags & TypeFlags.Primitive ? Ternary.False : Ternary.True); + const include: Ternary[] = types.map(t => t.flags & TypeFlags.Primitive || getReducedType(t).flags & TypeFlags.Never ? Ternary.False : Ternary.True); for (const [getDiscriminatingType, propertyName] of discriminators) { // If the remaining target types include at least one with a matching discriminant, eliminate those that // have non-matching discriminants. This ensures that we ignore erroneous discriminators and gradually diff --git a/tests/baselines/reference/contextuallyTypedByDiscriminableUnion2.symbols b/tests/baselines/reference/contextuallyTypedByDiscriminableUnion2.symbols new file mode 100644 index 0000000000000..9f68d9708cdf6 --- /dev/null +++ b/tests/baselines/reference/contextuallyTypedByDiscriminableUnion2.symbols @@ -0,0 +1,122 @@ +//// [tests/cases/compiler/contextuallyTypedByDiscriminableUnion2.ts] //// + +=== contextuallyTypedByDiscriminableUnion2.ts === +// https://github.com/microsoft/TypeScript/issues/62256 + +type Identifiable = { id: string }; +>Identifiable : Symbol(Identifiable, Decl(contextuallyTypedByDiscriminableUnion2.ts, 0, 0)) +>id : Symbol(id, Decl(contextuallyTypedByDiscriminableUnion2.ts, 2, 21)) + +interface EnableA { +>EnableA : Symbol(EnableA, Decl(contextuallyTypedByDiscriminableUnion2.ts, 2, 35)) + + readonly enableA: true; +>enableA : Symbol(EnableA.enableA, Decl(contextuallyTypedByDiscriminableUnion2.ts, 4, 19)) + + // this introduces a conflicting property with some of the other members of MyComponentProps + // making relevant final union members reduced nevers + readonly enableB: true; +>enableB : Symbol(EnableA.enableB, Decl(contextuallyTypedByDiscriminableUnion2.ts, 5, 25)) +} + +interface DisableA { +>DisableA : Symbol(DisableA, Decl(contextuallyTypedByDiscriminableUnion2.ts, 9, 1)) + + readonly enableA?: false; +>enableA : Symbol(DisableA.enableA, Decl(contextuallyTypedByDiscriminableUnion2.ts, 11, 20)) +} + +interface EnableB { +>EnableB : Symbol(EnableB, Decl(contextuallyTypedByDiscriminableUnion2.ts, 13, 1)) + + readonly enableB?: true; +>enableB : Symbol(EnableB.enableB, Decl(contextuallyTypedByDiscriminableUnion2.ts, 15, 19)) +} + +interface DisableB { +>DisableB : Symbol(DisableB, Decl(contextuallyTypedByDiscriminableUnion2.ts, 17, 1)) + + readonly enableB: false; +>enableB : Symbol(DisableB.enableB, Decl(contextuallyTypedByDiscriminableUnion2.ts, 19, 20)) +} + +export interface EnableD { +>EnableD : Symbol(EnableD, Decl(contextuallyTypedByDiscriminableUnion2.ts, 21, 1)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 23, 25)) +>Identifiable : Symbol(Identifiable, Decl(contextuallyTypedByDiscriminableUnion2.ts, 0, 0)) + + readonly enableD: true; +>enableD : Symbol(EnableD.enableD, Decl(contextuallyTypedByDiscriminableUnion2.ts, 23, 50)) + + readonly value: I["id"] | null; +>value : Symbol(EnableD.value, Decl(contextuallyTypedByDiscriminableUnion2.ts, 24, 25)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 23, 25)) + + readonly setItem: (item: I | null) => void; +>setItem : Symbol(EnableD.setItem, Decl(contextuallyTypedByDiscriminableUnion2.ts, 25, 33)) +>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion2.ts, 26, 21)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 23, 25)) +} + +export interface DisableD { +>DisableD : Symbol(DisableD, Decl(contextuallyTypedByDiscriminableUnion2.ts, 27, 1)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 29, 26)) +>Identifiable : Symbol(Identifiable, Decl(contextuallyTypedByDiscriminableUnion2.ts, 0, 0)) + + readonly enableD: false; +>enableD : Symbol(DisableD.enableD, Decl(contextuallyTypedByDiscriminableUnion2.ts, 29, 51)) + + readonly value: I["id"]; +>value : Symbol(DisableD.value, Decl(contextuallyTypedByDiscriminableUnion2.ts, 30, 26)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 29, 26)) + + readonly setItem: (item: I) => void; +>setItem : Symbol(DisableD.setItem, Decl(contextuallyTypedByDiscriminableUnion2.ts, 31, 26)) +>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion2.ts, 32, 21)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 29, 26)) +} + +type MyComponentProps = (EnableA | DisableA) & +>MyComponentProps : Symbol(MyComponentProps, Decl(contextuallyTypedByDiscriminableUnion2.ts, 33, 1)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 35, 22)) +>Identifiable : Symbol(Identifiable, Decl(contextuallyTypedByDiscriminableUnion2.ts, 0, 0)) +>EnableA : Symbol(EnableA, Decl(contextuallyTypedByDiscriminableUnion2.ts, 2, 35)) +>DisableA : Symbol(DisableA, Decl(contextuallyTypedByDiscriminableUnion2.ts, 9, 1)) + + (EnableB | DisableB) & +>EnableB : Symbol(EnableB, Decl(contextuallyTypedByDiscriminableUnion2.ts, 13, 1)) +>DisableB : Symbol(DisableB, Decl(contextuallyTypedByDiscriminableUnion2.ts, 17, 1)) + + (DisableD | EnableD); +>DisableD : Symbol(DisableD, Decl(contextuallyTypedByDiscriminableUnion2.ts, 27, 1)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 35, 22)) +>EnableD : Symbol(EnableD, Decl(contextuallyTypedByDiscriminableUnion2.ts, 21, 1)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 35, 22)) + +const MyComponent = (props: MyComponentProps) => {}; +>MyComponent : Symbol(MyComponent, Decl(contextuallyTypedByDiscriminableUnion2.ts, 39, 5)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 39, 21)) +>Identifiable : Symbol(Identifiable, Decl(contextuallyTypedByDiscriminableUnion2.ts, 0, 0)) +>props : Symbol(props, Decl(contextuallyTypedByDiscriminableUnion2.ts, 39, 45)) +>MyComponentProps : Symbol(MyComponentProps, Decl(contextuallyTypedByDiscriminableUnion2.ts, 33, 1)) +>I : Symbol(I, Decl(contextuallyTypedByDiscriminableUnion2.ts, 39, 21)) + +declare const item: string | null; +>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion2.ts, 41, 13)) + +MyComponent({ +>MyComponent : Symbol(MyComponent, Decl(contextuallyTypedByDiscriminableUnion2.ts, 39, 5)) + + enableD: true, +>enableD : Symbol(enableD, Decl(contextuallyTypedByDiscriminableUnion2.ts, 43, 13)) + + value: item, +>value : Symbol(value, Decl(contextuallyTypedByDiscriminableUnion2.ts, 44, 16)) +>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion2.ts, 41, 13)) + + setItem: (item) => {}, +>setItem : Symbol(setItem, Decl(contextuallyTypedByDiscriminableUnion2.ts, 45, 14)) +>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion2.ts, 46, 12)) + +}); + diff --git a/tests/baselines/reference/contextuallyTypedByDiscriminableUnion2.types b/tests/baselines/reference/contextuallyTypedByDiscriminableUnion2.types new file mode 100644 index 0000000000000..41b487e1103e5 --- /dev/null +++ b/tests/baselines/reference/contextuallyTypedByDiscriminableUnion2.types @@ -0,0 +1,136 @@ +//// [tests/cases/compiler/contextuallyTypedByDiscriminableUnion2.ts] //// + +=== contextuallyTypedByDiscriminableUnion2.ts === +// https://github.com/microsoft/TypeScript/issues/62256 + +type Identifiable = { id: string }; +>Identifiable : Identifiable +> : ^^^^^^^^^^^^ +>id : string +> : ^^^^^^ + +interface EnableA { + readonly enableA: true; +>enableA : true +> : ^^^^ +>true : true +> : ^^^^ + + // this introduces a conflicting property with some of the other members of MyComponentProps + // making relevant final union members reduced nevers + readonly enableB: true; +>enableB : true +> : ^^^^ +>true : true +> : ^^^^ +} + +interface DisableA { + readonly enableA?: false; +>enableA : false | undefined +> : ^^^^^^^^^^^^^^^^^ +>false : false +> : ^^^^^ +} + +interface EnableB { + readonly enableB?: true; +>enableB : true | undefined +> : ^^^^^^^^^^^^^^^^ +>true : true +> : ^^^^ +} + +interface DisableB { + readonly enableB: false; +>enableB : false +> : ^^^^^ +>false : false +> : ^^^^^ +} + +export interface EnableD { + readonly enableD: true; +>enableD : true +> : ^^^^ +>true : true +> : ^^^^ + + readonly value: I["id"] | null; +>value : I["id"] | null +> : ^^^^^^^^^^^^^^ + + readonly setItem: (item: I | null) => void; +>setItem : (item: I | null) => void +> : ^ ^^ ^^^^^ +>item : I | null +> : ^^^^^^^^ +} + +export interface DisableD { + readonly enableD: false; +>enableD : false +> : ^^^^^ +>false : false +> : ^^^^^ + + readonly value: I["id"]; +>value : I["id"] +> : ^^^^^^^ + + readonly setItem: (item: I) => void; +>setItem : (item: I) => void +> : ^ ^^ ^^^^^ +>item : I +> : ^ +} + +type MyComponentProps = (EnableA | DisableA) & +>MyComponentProps : (EnableA & EnableB & DisableD) | (EnableA & EnableB & EnableD) | (DisableA & EnableB & DisableD) | (DisableA & EnableB & EnableD) | (DisableA & DisableB & DisableD) | (DisableA & DisableB & EnableD) +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + (EnableB | DisableB) & + (DisableD | EnableD); + +const MyComponent = (props: MyComponentProps) => {}; +>MyComponent : (props: MyComponentProps) => void +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^^^^^ +>(props: MyComponentProps) => {} : (props: MyComponentProps) => void +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^^^^^ +>props : (EnableA & EnableB & DisableD) | (EnableA & EnableB & EnableD) | (DisableA & EnableB & DisableD) | (DisableA & EnableB & EnableD) | (DisableA & DisableB & DisableD) | (DisableA & DisableB & EnableD) +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +declare const item: string | null; +>item : string | null +> : ^^^^^^^^^^^^^ + +MyComponent({ +>MyComponent({ enableD: true, value: item, setItem: (item) => {},}) : void +> : ^^^^ +>MyComponent : (props: MyComponentProps) => void +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^^^^^ +>{ enableD: true, value: item, setItem: (item) => {},} : { enableD: true; value: string | null; setItem: (item: Identifiable | null) => void; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + enableD: true, +>enableD : true +> : ^^^^ +>true : true +> : ^^^^ + + value: item, +>value : string | null +> : ^^^^^^^^^^^^^ +>item : string | null +> : ^^^^^^^^^^^^^ + + setItem: (item) => {}, +>setItem : (item: Identifiable | null) => void +> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>(item) => {} : (item: Identifiable | null) => void +> : ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>item : Identifiable | null +> : ^^^^^^^^^^^^^^^^^^^ + +}); + diff --git a/tests/cases/compiler/contextuallyTypedByDiscriminableUnion2.ts b/tests/cases/compiler/contextuallyTypedByDiscriminableUnion2.ts new file mode 100644 index 0000000000000..498199d268b94 --- /dev/null +++ b/tests/cases/compiler/contextuallyTypedByDiscriminableUnion2.ts @@ -0,0 +1,51 @@ +// @strict: true +// @noEmit: true + +// https://github.com/microsoft/TypeScript/issues/62256 + +type Identifiable = { id: string }; + +interface EnableA { + readonly enableA: true; + // this introduces a conflicting property with some of the other members of MyComponentProps + // making relevant final union members reduced nevers + readonly enableB: true; +} + +interface DisableA { + readonly enableA?: false; +} + +interface EnableB { + readonly enableB?: true; +} + +interface DisableB { + readonly enableB: false; +} + +export interface EnableD { + readonly enableD: true; + readonly value: I["id"] | null; + readonly setItem: (item: I | null) => void; +} + +export interface DisableD { + readonly enableD: false; + readonly value: I["id"]; + readonly setItem: (item: I) => void; +} + +type MyComponentProps = (EnableA | DisableA) & + (EnableB | DisableB) & + (DisableD | EnableD); + +const MyComponent = (props: MyComponentProps) => {}; + +declare const item: string | null; + +MyComponent({ + enableD: true, + value: item, + setItem: (item) => {}, +});