diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 30701e54ae7..69933033d39 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -27677,13 +27677,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) { - const type = !(computedType.flags & TypeFlags.Union) && declaredType.flags & TypeFlags.Union ? declaredType : computedType; - if (type.flags & TypeFlags.Union) { + // As long as the computed type is a subset of the declared type, we use the full declared type to detect + // a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type + // predicate narrowing, we use the actual computed type. + if (declaredType.flags & TypeFlags.Union || computedType.flags & TypeFlags.Union) { const access = getCandidateDiscriminantPropertyAccess(expr); if (access) { const name = getAccessedPropertyName(access); - if (name && isDiscriminantProperty(type, name)) { - return access; + if (name) { + const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType; + if (isDiscriminantProperty(type, name)) { + return access; + } } } } diff --git a/tests/baselines/reference/narrowingUnionToUnion.js b/tests/baselines/reference/narrowingUnionToUnion.js index e623d346c0b..efe6819d094 100644 --- a/tests/baselines/reference/narrowingUnionToUnion.js +++ b/tests/baselines/reference/narrowingUnionToUnion.js @@ -226,6 +226,43 @@ declare const workingAgain: Record | undefined | unknown; isMyDiscriminatedUnion(working) && working.type === 'A' && working.aProp; isMyDiscriminatedUnion(broken) && broken.type === 'A' && broken.aProp; isMyDiscriminatedUnion(workingAgain) && workingAgain.type === 'A' && workingAgain.aProp; + +// Repro from #56144 + +type Union = + | { type: 'a'; variant: 1 } + | { type: 'a'; variant: 2 } + | { type: 'b' }; + +function example1(value: Union): { type: 'a'; variant: 2 } | null { + if (value.type !== 'a') { + return null; + } + if (value.variant === 1) { + return null; + } + return value; +} + +function example2(value: Union): { type: 'a'; variant: 2 } | null { + if (value.type !== 'a') { + return null; + } + if (value.type === 'a' && value.variant === 1) { + return null; + } + return value; +} + +function example3(value: Union): { type: 'a'; variant: 2 } | null { + if (value.type !== 'a') { + return null; + } + if (value.type && value.variant === 1) { + return null; + } + return value; +} //// [narrowingUnionToUnion.js] @@ -387,6 +424,33 @@ function f1x(obj) { isMyDiscriminatedUnion(working) && working.type === 'A' && working.aProp; isMyDiscriminatedUnion(broken) && broken.type === 'A' && broken.aProp; isMyDiscriminatedUnion(workingAgain) && workingAgain.type === 'A' && workingAgain.aProp; +function example1(value) { + if (value.type !== 'a') { + return null; + } + if (value.variant === 1) { + return null; + } + return value; +} +function example2(value) { + if (value.type !== 'a') { + return null; + } + if (value.type === 'a' && value.variant === 1) { + return null; + } + return value; +} +function example3(value) { + if (value.type !== 'a') { + return null; + } + if (value.type && value.variant === 1) { + return null; + } + return value; +} //// [narrowingUnionToUnion.d.ts] @@ -451,3 +515,24 @@ declare function isMyDiscriminatedUnion(item: unknown): item is MyDiscriminatedU declare const working: unknown; declare const broken: Record | undefined; declare const workingAgain: Record | undefined | unknown; +type Union = { + type: 'a'; + variant: 1; +} | { + type: 'a'; + variant: 2; +} | { + type: 'b'; +}; +declare function example1(value: Union): { + type: 'a'; + variant: 2; +} | null; +declare function example2(value: Union): { + type: 'a'; + variant: 2; +} | null; +declare function example3(value: Union): { + type: 'a'; + variant: 2; +} | null; diff --git a/tests/baselines/reference/narrowingUnionToUnion.symbols b/tests/baselines/reference/narrowingUnionToUnion.symbols index e6ad800b8fe..3de33f45c90 100644 --- a/tests/baselines/reference/narrowingUnionToUnion.symbols +++ b/tests/baselines/reference/narrowingUnionToUnion.symbols @@ -538,3 +538,100 @@ isMyDiscriminatedUnion(workingAgain) && workingAgain.type === 'A' && workingAgai >workingAgain : Symbol(workingAgain, Decl(narrowingUnionToUnion.ts, 220, 13)) >aProp : Symbol(aProp, Decl(narrowingUnionToUnion.ts, 214, 40)) +// Repro from #56144 + +type Union = +>Union : Symbol(Union, Decl(narrowingUnionToUnion.ts, 224, 88)) + + | { type: 'a'; variant: 1 } +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7)) +>variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 229, 18)) + + | { type: 'a'; variant: 2 } +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 230, 7)) +>variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 230, 18)) + + | { type: 'b' }; +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 231, 7)) + +function example1(value: Union): { type: 'a'; variant: 2 } | null { +>example1 : Symbol(example1, Decl(narrowingUnionToUnion.ts, 231, 20)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 233, 18)) +>Union : Symbol(Union, Decl(narrowingUnionToUnion.ts, 224, 88)) +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 233, 34)) +>variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 233, 45)) + + if (value.type !== 'a') { +>value.type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7), Decl(narrowingUnionToUnion.ts, 231, 7)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 233, 18)) +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7), Decl(narrowingUnionToUnion.ts, 231, 7)) + + return null; + } + if (value.variant === 1) { +>value.variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 229, 18), Decl(narrowingUnionToUnion.ts, 230, 18)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 233, 18)) +>variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 229, 18), Decl(narrowingUnionToUnion.ts, 230, 18)) + + return null; + } + return value; +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 233, 18)) +} + +function example2(value: Union): { type: 'a'; variant: 2 } | null { +>example2 : Symbol(example2, Decl(narrowingUnionToUnion.ts, 241, 1)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 243, 18)) +>Union : Symbol(Union, Decl(narrowingUnionToUnion.ts, 224, 88)) +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 243, 34)) +>variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 243, 45)) + + if (value.type !== 'a') { +>value.type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7), Decl(narrowingUnionToUnion.ts, 231, 7)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 243, 18)) +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7), Decl(narrowingUnionToUnion.ts, 231, 7)) + + return null; + } + if (value.type === 'a' && value.variant === 1) { +>value.type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 243, 18)) +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7)) +>value.variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 229, 18), Decl(narrowingUnionToUnion.ts, 230, 18)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 243, 18)) +>variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 229, 18), Decl(narrowingUnionToUnion.ts, 230, 18)) + + return null; + } + return value; +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 243, 18)) +} + +function example3(value: Union): { type: 'a'; variant: 2 } | null { +>example3 : Symbol(example3, Decl(narrowingUnionToUnion.ts, 251, 1)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 253, 18)) +>Union : Symbol(Union, Decl(narrowingUnionToUnion.ts, 224, 88)) +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 253, 34)) +>variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 253, 45)) + + if (value.type !== 'a') { +>value.type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7), Decl(narrowingUnionToUnion.ts, 231, 7)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 253, 18)) +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7), Decl(narrowingUnionToUnion.ts, 231, 7)) + + return null; + } + if (value.type && value.variant === 1) { +>value.type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 253, 18)) +>type : Symbol(type, Decl(narrowingUnionToUnion.ts, 229, 7), Decl(narrowingUnionToUnion.ts, 230, 7)) +>value.variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 229, 18), Decl(narrowingUnionToUnion.ts, 230, 18)) +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 253, 18)) +>variant : Symbol(variant, Decl(narrowingUnionToUnion.ts, 229, 18), Decl(narrowingUnionToUnion.ts, 230, 18)) + + return null; + } + return value; +>value : Symbol(value, Decl(narrowingUnionToUnion.ts, 253, 18)) +} + diff --git a/tests/baselines/reference/narrowingUnionToUnion.types b/tests/baselines/reference/narrowingUnionToUnion.types index 0371411ea74..07ecb01d822 100644 --- a/tests/baselines/reference/narrowingUnionToUnion.types +++ b/tests/baselines/reference/narrowingUnionToUnion.types @@ -574,3 +574,113 @@ isMyDiscriminatedUnion(workingAgain) && workingAgain.type === 'A' && workingAgai >workingAgain : { type: "A"; aProp: number; } >aProp : number +// Repro from #56144 + +type Union = +>Union : { type: 'a'; variant: 1; } | { type: 'a'; variant: 2; } | { type: 'b'; } + + | { type: 'a'; variant: 1 } +>type : "a" +>variant : 1 + + | { type: 'a'; variant: 2 } +>type : "a" +>variant : 2 + + | { type: 'b' }; +>type : "b" + +function example1(value: Union): { type: 'a'; variant: 2 } | null { +>example1 : (value: Union) => { type: 'a'; variant: 2;} | null +>value : Union +>type : "a" +>variant : 2 + + if (value.type !== 'a') { +>value.type !== 'a' : boolean +>value.type : "a" | "b" +>value : Union +>type : "a" | "b" +>'a' : "a" + + return null; + } + if (value.variant === 1) { +>value.variant === 1 : boolean +>value.variant : 1 | 2 +>value : { type: "a"; variant: 1; } | { type: "a"; variant: 2; } +>variant : 1 | 2 +>1 : 1 + + return null; + } + return value; +>value : { type: "a"; variant: 2; } +} + +function example2(value: Union): { type: 'a'; variant: 2 } | null { +>example2 : (value: Union) => { type: 'a'; variant: 2;} | null +>value : Union +>type : "a" +>variant : 2 + + if (value.type !== 'a') { +>value.type !== 'a' : boolean +>value.type : "a" | "b" +>value : Union +>type : "a" | "b" +>'a' : "a" + + return null; + } + if (value.type === 'a' && value.variant === 1) { +>value.type === 'a' && value.variant === 1 : boolean +>value.type === 'a' : boolean +>value.type : "a" +>value : { type: "a"; variant: 1; } | { type: "a"; variant: 2; } +>type : "a" +>'a' : "a" +>value.variant === 1 : boolean +>value.variant : 1 | 2 +>value : { type: "a"; variant: 1; } | { type: "a"; variant: 2; } +>variant : 1 | 2 +>1 : 1 + + return null; + } + return value; +>value : { type: "a"; variant: 2; } +} + +function example3(value: Union): { type: 'a'; variant: 2 } | null { +>example3 : (value: Union) => { type: 'a'; variant: 2;} | null +>value : Union +>type : "a" +>variant : 2 + + if (value.type !== 'a') { +>value.type !== 'a' : boolean +>value.type : "a" | "b" +>value : Union +>type : "a" | "b" +>'a' : "a" + + return null; + } + if (value.type && value.variant === 1) { +>value.type && value.variant === 1 : boolean +>value.type : "a" +>value : { type: "a"; variant: 1; } | { type: "a"; variant: 2; } +>type : "a" +>value.variant === 1 : boolean +>value.variant : 1 | 2 +>value : { type: "a"; variant: 1; } | { type: "a"; variant: 2; } +>variant : 1 | 2 +>1 : 1 + + return null; + } + return value; +>value : { type: "a"; variant: 2; } +} + diff --git a/tests/cases/compiler/narrowingUnionToUnion.ts b/tests/cases/compiler/narrowingUnionToUnion.ts index c4e2682e3c9..1ecd613d3d6 100644 --- a/tests/cases/compiler/narrowingUnionToUnion.ts +++ b/tests/cases/compiler/narrowingUnionToUnion.ts @@ -226,3 +226,40 @@ declare const workingAgain: Record | undefined | unknown; isMyDiscriminatedUnion(working) && working.type === 'A' && working.aProp; isMyDiscriminatedUnion(broken) && broken.type === 'A' && broken.aProp; isMyDiscriminatedUnion(workingAgain) && workingAgain.type === 'A' && workingAgain.aProp; + +// Repro from #56144 + +type Union = + | { type: 'a'; variant: 1 } + | { type: 'a'; variant: 2 } + | { type: 'b' }; + +function example1(value: Union): { type: 'a'; variant: 2 } | null { + if (value.type !== 'a') { + return null; + } + if (value.variant === 1) { + return null; + } + return value; +} + +function example2(value: Union): { type: 'a'; variant: 2 } | null { + if (value.type !== 'a') { + return null; + } + if (value.type === 'a' && value.variant === 1) { + return null; + } + return value; +} + +function example3(value: Union): { type: 'a'; variant: 2 } | null { + if (value.type !== 'a') { + return null; + } + if (value.type && value.variant === 1) { + return null; + } + return value; +}