diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c788c6807a9e2..fad306a4d5a8f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20587,14 +20587,15 @@ namespace ts { } function isMatchingReferenceDiscriminant(expr: Expression, computedType: Type) { - if (!(computedType.flags & TypeFlags.Union) || !isAccessExpression(expr)) { + const type = declaredType.flags & TypeFlags.Union ? declaredType : computedType; + if (!(type.flags & TypeFlags.Union) || !isAccessExpression(expr)) { return false; } const name = getAccessedPropertyName(expr); if (name === undefined) { return false; } - return isMatchingReference(reference, expr.expression) && isDiscriminantProperty(computedType, name); + return isMatchingReference(reference, expr.expression) && isDiscriminantProperty(type, name); } function narrowTypeByDiscriminant(type: Type, access: AccessExpression, narrowType: (t: Type) => Type): Type { @@ -20620,7 +20621,7 @@ namespace ts { if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) { type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); } - if (isMatchingReferenceDiscriminant(expr, declaredType)) { + if (isMatchingReferenceDiscriminant(expr, type)) { return narrowTypeByDiscriminant(type, expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy)); } return type; @@ -20676,10 +20677,10 @@ namespace ts { type = narrowTypeByOptionalChainContainment(type, operator, left, assumeTrue); } } - if (isMatchingReferenceDiscriminant(left, declaredType)) { + if (isMatchingReferenceDiscriminant(left, type)) { return narrowTypeByDiscriminant(type, left, t => narrowTypeByEquality(t, operator, right, assumeTrue)); } - if (isMatchingReferenceDiscriminant(right, declaredType)) { + if (isMatchingReferenceDiscriminant(right, type)) { return narrowTypeByDiscriminant(type, right, t => narrowTypeByEquality(t, operator, left, assumeTrue)); } if (isMatchingConstructorReference(left)) { @@ -21101,7 +21102,7 @@ namespace ts { !(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) { type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); } - if (isMatchingReferenceDiscriminant(predicateArgument, declaredType)) { + if (isMatchingReferenceDiscriminant(predicateArgument, type)) { return narrowTypeByDiscriminant(type, predicateArgument as AccessExpression, t => getNarrowedType(t, predicate.type!, assumeTrue, isTypeSubtypeOf)); } } @@ -21143,7 +21144,7 @@ namespace ts { if (isMatchingReference(reference, expr)) { return getTypeWithFacts(type, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull); } - if (isMatchingReferenceDiscriminant(expr, declaredType)) { + if (isMatchingReferenceDiscriminant(expr, type)) { return narrowTypeByDiscriminant(type, expr, t => getTypeWithFacts(t, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull)); } return type; diff --git a/tests/baselines/reference/discriminantPropertyCheck.js b/tests/baselines/reference/discriminantPropertyCheck.js index ee1c255ed28b4..cbbaa80d2ca3f 100644 --- a/tests/baselines/reference/discriminantPropertyCheck.js +++ b/tests/baselines/reference/discriminantPropertyCheck.js @@ -174,6 +174,67 @@ function func3(value: Partial) { } } } + +// Repro from #30557 + +interface TypeA { + Name: "TypeA"; + Value1: "Cool stuff!"; +} + +interface TypeB { + Name: "TypeB"; + Value2: 0; +} + +type Type = TypeA | TypeB; + +declare function isType(x: unknown): x is Type; + +function WorksProperly(data: Type) { + if (data.Name === "TypeA") { + const value1 = data.Value1; + } +} + +function DoesNotWork(data: unknown) { + if (isType(data)) { + if (data.Name === "TypeA") { + const value1 = data.Value1; + } + } +} + +// Repro from #36777 + +type TestA = { + type: 'testA'; + bananas: 3; +} + +type TestB = { + type: 'testB'; + apples: 5; +} + +type AllTests = TestA | TestB; + +type MapOfAllTests = Record; + +const doTestingStuff = (mapOfTests: MapOfAllTests, ids: string[]) => { + ids.forEach(id => { + let test; + test = mapOfTests[id]; + if (test.type === 'testA') { + console.log(test.bananas); + } + switch (test.type) { + case 'testA': { + console.log(test.bananas); + } + } + }); +}; //// [discriminantPropertyCheck.js] @@ -269,3 +330,29 @@ function func3(value) { } } } +function WorksProperly(data) { + if (data.Name === "TypeA") { + var value1 = data.Value1; + } +} +function DoesNotWork(data) { + if (isType(data)) { + if (data.Name === "TypeA") { + var value1 = data.Value1; + } + } +} +var doTestingStuff = function (mapOfTests, ids) { + ids.forEach(function (id) { + var test; + test = mapOfTests[id]; + if (test.type === 'testA') { + console.log(test.bananas); + } + switch (test.type) { + case 'testA': { + console.log(test.bananas); + } + } + }); +}; diff --git a/tests/baselines/reference/discriminantPropertyCheck.symbols b/tests/baselines/reference/discriminantPropertyCheck.symbols index b02a8113d46c2..7fa10ac46e28e 100644 --- a/tests/baselines/reference/discriminantPropertyCheck.symbols +++ b/tests/baselines/reference/discriminantPropertyCheck.symbols @@ -508,3 +508,159 @@ function func3(value: Partial) { } } +// Repro from #30557 + +interface TypeA { +>TypeA : Symbol(TypeA, Decl(discriminantPropertyCheck.ts, 174, 1)) + + Name: "TypeA"; +>Name : Symbol(TypeA.Name, Decl(discriminantPropertyCheck.ts, 178, 17)) + + Value1: "Cool stuff!"; +>Value1 : Symbol(TypeA.Value1, Decl(discriminantPropertyCheck.ts, 179, 18)) +} + +interface TypeB { +>TypeB : Symbol(TypeB, Decl(discriminantPropertyCheck.ts, 181, 1)) + + Name: "TypeB"; +>Name : Symbol(TypeB.Name, Decl(discriminantPropertyCheck.ts, 183, 17)) + + Value2: 0; +>Value2 : Symbol(TypeB.Value2, Decl(discriminantPropertyCheck.ts, 184, 18)) +} + +type Type = TypeA | TypeB; +>Type : Symbol(Type, Decl(discriminantPropertyCheck.ts, 186, 1)) +>TypeA : Symbol(TypeA, Decl(discriminantPropertyCheck.ts, 174, 1)) +>TypeB : Symbol(TypeB, Decl(discriminantPropertyCheck.ts, 181, 1)) + +declare function isType(x: unknown): x is Type; +>isType : Symbol(isType, Decl(discriminantPropertyCheck.ts, 188, 26)) +>x : Symbol(x, Decl(discriminantPropertyCheck.ts, 190, 24)) +>x : Symbol(x, Decl(discriminantPropertyCheck.ts, 190, 24)) +>Type : Symbol(Type, Decl(discriminantPropertyCheck.ts, 186, 1)) + +function WorksProperly(data: Type) { +>WorksProperly : Symbol(WorksProperly, Decl(discriminantPropertyCheck.ts, 190, 47)) +>data : Symbol(data, Decl(discriminantPropertyCheck.ts, 192, 23)) +>Type : Symbol(Type, Decl(discriminantPropertyCheck.ts, 186, 1)) + + if (data.Name === "TypeA") { +>data.Name : Symbol(Name, Decl(discriminantPropertyCheck.ts, 178, 17), Decl(discriminantPropertyCheck.ts, 183, 17)) +>data : Symbol(data, Decl(discriminantPropertyCheck.ts, 192, 23)) +>Name : Symbol(Name, Decl(discriminantPropertyCheck.ts, 178, 17), Decl(discriminantPropertyCheck.ts, 183, 17)) + + const value1 = data.Value1; +>value1 : Symbol(value1, Decl(discriminantPropertyCheck.ts, 194, 13)) +>data.Value1 : Symbol(TypeA.Value1, Decl(discriminantPropertyCheck.ts, 179, 18)) +>data : Symbol(data, Decl(discriminantPropertyCheck.ts, 192, 23)) +>Value1 : Symbol(TypeA.Value1, Decl(discriminantPropertyCheck.ts, 179, 18)) + } +} + +function DoesNotWork(data: unknown) { +>DoesNotWork : Symbol(DoesNotWork, Decl(discriminantPropertyCheck.ts, 196, 1)) +>data : Symbol(data, Decl(discriminantPropertyCheck.ts, 198, 21)) + + if (isType(data)) { +>isType : Symbol(isType, Decl(discriminantPropertyCheck.ts, 188, 26)) +>data : Symbol(data, Decl(discriminantPropertyCheck.ts, 198, 21)) + + if (data.Name === "TypeA") { +>data.Name : Symbol(Name, Decl(discriminantPropertyCheck.ts, 178, 17), Decl(discriminantPropertyCheck.ts, 183, 17)) +>data : Symbol(data, Decl(discriminantPropertyCheck.ts, 198, 21)) +>Name : Symbol(Name, Decl(discriminantPropertyCheck.ts, 178, 17), Decl(discriminantPropertyCheck.ts, 183, 17)) + + const value1 = data.Value1; +>value1 : Symbol(value1, Decl(discriminantPropertyCheck.ts, 201, 17)) +>data.Value1 : Symbol(TypeA.Value1, Decl(discriminantPropertyCheck.ts, 179, 18)) +>data : Symbol(data, Decl(discriminantPropertyCheck.ts, 198, 21)) +>Value1 : Symbol(TypeA.Value1, Decl(discriminantPropertyCheck.ts, 179, 18)) + } + } +} + +// Repro from #36777 + +type TestA = { +>TestA : Symbol(TestA, Decl(discriminantPropertyCheck.ts, 204, 1)) + + type: 'testA'; +>type : Symbol(type, Decl(discriminantPropertyCheck.ts, 208, 14)) + + bananas: 3; +>bananas : Symbol(bananas, Decl(discriminantPropertyCheck.ts, 209, 18)) +} + +type TestB = { +>TestB : Symbol(TestB, Decl(discriminantPropertyCheck.ts, 211, 1)) + + type: 'testB'; +>type : Symbol(type, Decl(discriminantPropertyCheck.ts, 213, 14)) + + apples: 5; +>apples : Symbol(apples, Decl(discriminantPropertyCheck.ts, 214, 18)) +} + +type AllTests = TestA | TestB; +>AllTests : Symbol(AllTests, Decl(discriminantPropertyCheck.ts, 216, 1)) +>TestA : Symbol(TestA, Decl(discriminantPropertyCheck.ts, 204, 1)) +>TestB : Symbol(TestB, Decl(discriminantPropertyCheck.ts, 211, 1)) + +type MapOfAllTests = Record; +>MapOfAllTests : Symbol(MapOfAllTests, Decl(discriminantPropertyCheck.ts, 218, 30)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>AllTests : Symbol(AllTests, Decl(discriminantPropertyCheck.ts, 216, 1)) + +const doTestingStuff = (mapOfTests: MapOfAllTests, ids: string[]) => { +>doTestingStuff : Symbol(doTestingStuff, Decl(discriminantPropertyCheck.ts, 222, 5)) +>mapOfTests : Symbol(mapOfTests, Decl(discriminantPropertyCheck.ts, 222, 24)) +>MapOfAllTests : Symbol(MapOfAllTests, Decl(discriminantPropertyCheck.ts, 218, 30)) +>ids : Symbol(ids, Decl(discriminantPropertyCheck.ts, 222, 50)) + + ids.forEach(id => { +>ids.forEach : Symbol(Array.forEach, Decl(lib.es5.d.ts, --, --)) +>ids : Symbol(ids, Decl(discriminantPropertyCheck.ts, 222, 50)) +>forEach : Symbol(Array.forEach, Decl(lib.es5.d.ts, --, --)) +>id : Symbol(id, Decl(discriminantPropertyCheck.ts, 223, 16)) + + let test; +>test : Symbol(test, Decl(discriminantPropertyCheck.ts, 224, 11)) + + test = mapOfTests[id]; +>test : Symbol(test, Decl(discriminantPropertyCheck.ts, 224, 11)) +>mapOfTests : Symbol(mapOfTests, Decl(discriminantPropertyCheck.ts, 222, 24)) +>id : Symbol(id, Decl(discriminantPropertyCheck.ts, 223, 16)) + + if (test.type === 'testA') { +>test.type : Symbol(type, Decl(discriminantPropertyCheck.ts, 208, 14), Decl(discriminantPropertyCheck.ts, 213, 14)) +>test : Symbol(test, Decl(discriminantPropertyCheck.ts, 224, 11)) +>type : Symbol(type, Decl(discriminantPropertyCheck.ts, 208, 14), Decl(discriminantPropertyCheck.ts, 213, 14)) + + console.log(test.bananas); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>test.bananas : Symbol(bananas, Decl(discriminantPropertyCheck.ts, 209, 18)) +>test : Symbol(test, Decl(discriminantPropertyCheck.ts, 224, 11)) +>bananas : Symbol(bananas, Decl(discriminantPropertyCheck.ts, 209, 18)) + } + switch (test.type) { +>test.type : Symbol(type, Decl(discriminantPropertyCheck.ts, 208, 14), Decl(discriminantPropertyCheck.ts, 213, 14)) +>test : Symbol(test, Decl(discriminantPropertyCheck.ts, 224, 11)) +>type : Symbol(type, Decl(discriminantPropertyCheck.ts, 208, 14), Decl(discriminantPropertyCheck.ts, 213, 14)) + + case 'testA': { + console.log(test.bananas); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>test.bananas : Symbol(bananas, Decl(discriminantPropertyCheck.ts, 209, 18)) +>test : Symbol(test, Decl(discriminantPropertyCheck.ts, 224, 11)) +>bananas : Symbol(bananas, Decl(discriminantPropertyCheck.ts, 209, 18)) + } + } + }); +}; + diff --git a/tests/baselines/reference/discriminantPropertyCheck.types b/tests/baselines/reference/discriminantPropertyCheck.types index 1e46a7dbe9dd2..da884470e6f14 100644 --- a/tests/baselines/reference/discriminantPropertyCheck.types +++ b/tests/baselines/reference/discriminantPropertyCheck.types @@ -501,3 +501,161 @@ function func3(value: Partial) { } } +// Repro from #30557 + +interface TypeA { + Name: "TypeA"; +>Name : "TypeA" + + Value1: "Cool stuff!"; +>Value1 : "Cool stuff!" +} + +interface TypeB { + Name: "TypeB"; +>Name : "TypeB" + + Value2: 0; +>Value2 : 0 +} + +type Type = TypeA | TypeB; +>Type : Type + +declare function isType(x: unknown): x is Type; +>isType : (x: unknown) => x is Type +>x : unknown + +function WorksProperly(data: Type) { +>WorksProperly : (data: Type) => void +>data : Type + + if (data.Name === "TypeA") { +>data.Name === "TypeA" : boolean +>data.Name : "TypeA" | "TypeB" +>data : Type +>Name : "TypeA" | "TypeB" +>"TypeA" : "TypeA" + + const value1 = data.Value1; +>value1 : "Cool stuff!" +>data.Value1 : "Cool stuff!" +>data : TypeA +>Value1 : "Cool stuff!" + } +} + +function DoesNotWork(data: unknown) { +>DoesNotWork : (data: unknown) => void +>data : unknown + + if (isType(data)) { +>isType(data) : boolean +>isType : (x: unknown) => x is Type +>data : unknown + + if (data.Name === "TypeA") { +>data.Name === "TypeA" : boolean +>data.Name : "TypeA" | "TypeB" +>data : Type +>Name : "TypeA" | "TypeB" +>"TypeA" : "TypeA" + + const value1 = data.Value1; +>value1 : "Cool stuff!" +>data.Value1 : "Cool stuff!" +>data : TypeA +>Value1 : "Cool stuff!" + } + } +} + +// Repro from #36777 + +type TestA = { +>TestA : TestA + + type: 'testA'; +>type : "testA" + + bananas: 3; +>bananas : 3 +} + +type TestB = { +>TestB : TestB + + type: 'testB'; +>type : "testB" + + apples: 5; +>apples : 5 +} + +type AllTests = TestA | TestB; +>AllTests : AllTests + +type MapOfAllTests = Record; +>MapOfAllTests : Record + +const doTestingStuff = (mapOfTests: MapOfAllTests, ids: string[]) => { +>doTestingStuff : (mapOfTests: MapOfAllTests, ids: string[]) => void +>(mapOfTests: MapOfAllTests, ids: string[]) => { ids.forEach(id => { let test; test = mapOfTests[id]; if (test.type === 'testA') { console.log(test.bananas); } switch (test.type) { case 'testA': { console.log(test.bananas); } } });} : (mapOfTests: MapOfAllTests, ids: string[]) => void +>mapOfTests : Record +>ids : string[] + + ids.forEach(id => { +>ids.forEach(id => { let test; test = mapOfTests[id]; if (test.type === 'testA') { console.log(test.bananas); } switch (test.type) { case 'testA': { console.log(test.bananas); } } }) : void +>ids.forEach : (callbackfn: (value: string, index: number, array: string[]) => void, thisArg?: any) => void +>ids : string[] +>forEach : (callbackfn: (value: string, index: number, array: string[]) => void, thisArg?: any) => void +>id => { let test; test = mapOfTests[id]; if (test.type === 'testA') { console.log(test.bananas); } switch (test.type) { case 'testA': { console.log(test.bananas); } } } : (id: string) => void +>id : string + + let test; +>test : any + + test = mapOfTests[id]; +>test = mapOfTests[id] : AllTests +>test : any +>mapOfTests[id] : AllTests +>mapOfTests : Record +>id : string + + if (test.type === 'testA') { +>test.type === 'testA' : boolean +>test.type : "testA" | "testB" +>test : AllTests +>type : "testA" | "testB" +>'testA' : "testA" + + console.log(test.bananas); +>console.log(test.bananas) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>test.bananas : 3 +>test : TestA +>bananas : 3 + } + switch (test.type) { +>test.type : "testA" | "testB" +>test : AllTests +>type : "testA" | "testB" + + case 'testA': { +>'testA' : "testA" + + console.log(test.bananas); +>console.log(test.bananas) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>test.bananas : 3 +>test : TestA +>bananas : 3 + } + } + }); +}; + diff --git a/tests/cases/compiler/discriminantPropertyCheck.ts b/tests/cases/compiler/discriminantPropertyCheck.ts index 6a15af5db65df..478bc5f3c296a 100644 --- a/tests/cases/compiler/discriminantPropertyCheck.ts +++ b/tests/cases/compiler/discriminantPropertyCheck.ts @@ -1,4 +1,4 @@ -// @strictNullChecks: true +// @strict: true type Item = Item1 | Item2; @@ -175,3 +175,64 @@ function func3(value: Partial) { } } } + +// Repro from #30557 + +interface TypeA { + Name: "TypeA"; + Value1: "Cool stuff!"; +} + +interface TypeB { + Name: "TypeB"; + Value2: 0; +} + +type Type = TypeA | TypeB; + +declare function isType(x: unknown): x is Type; + +function WorksProperly(data: Type) { + if (data.Name === "TypeA") { + const value1 = data.Value1; + } +} + +function DoesNotWork(data: unknown) { + if (isType(data)) { + if (data.Name === "TypeA") { + const value1 = data.Value1; + } + } +} + +// Repro from #36777 + +type TestA = { + type: 'testA'; + bananas: 3; +} + +type TestB = { + type: 'testB'; + apples: 5; +} + +type AllTests = TestA | TestB; + +type MapOfAllTests = Record; + +const doTestingStuff = (mapOfTests: MapOfAllTests, ids: string[]) => { + ids.forEach(id => { + let test; + test = mapOfTests[id]; + if (test.type === 'testA') { + console.log(test.bananas); + } + switch (test.type) { + case 'testA': { + console.log(test.bananas); + } + } + }); +};