From 366f92aff3445f9b2a9fca985609a89471c30ce8 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 6 Jan 2023 14:10:28 -0800 Subject: [PATCH 1/3] fix narrowTypeByDiscriminant for non null expression access --- src/compiler/checker.ts | 3 +- src/compiler/utilities.ts | 8 + .../reference/narrowingUnionWithBang.js | 85 ++++++ .../reference/narrowingUnionWithBang.symbols | 235 ++++++++++++++++ .../reference/narrowingUnionWithBang.types | 257 ++++++++++++++++++ .../cases/compiler/narrowingUnionWithBang.ts | 59 ++++ 6 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/narrowingUnionWithBang.js create mode 100644 tests/baselines/reference/narrowingUnionWithBang.symbols create mode 100644 tests/baselines/reference/narrowingUnionWithBang.types create mode 100644 tests/cases/compiler/narrowingUnionWithBang.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 30eb32a7ac48a..ff1b2e845bf64 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -603,6 +603,7 @@ import { isNewExpression, isNightly, isNodeDescendantOf, + isNonNullAccess, isNullishCoalesce, isNumericLiteral, isNumericLiteralName, @@ -26299,7 +26300,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (propName === undefined) { return type; } - const removeNullable = strictNullChecks && isOptionalChain(access) && maybeTypeOfKind(type, TypeFlags.Nullable); + const removeNullable = strictNullChecks && (isOptionalChain(access) || isNonNullAccess(access)) && maybeTypeOfKind(type, TypeFlags.Nullable); let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName); if (!propType) { return type; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 56b18ff90056d..0219352fe19b3 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -295,6 +295,7 @@ import { isNamespaceExport, isNamespaceExportDeclaration, isNamespaceImport, + isNonNullExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, @@ -9482,3 +9483,10 @@ export function isOptionalDeclaration(declaration: Declaration): boolean { return false; } } + +/** @internal */ +export function isNonNullAccess(node: Node): node is AccessExpression { + const kind = node.kind; + return (kind === SyntaxKind.PropertyAccessExpression + || kind === SyntaxKind.ElementAccessExpression) && isNonNullExpression((node as AccessExpression).expression); +} diff --git a/tests/baselines/reference/narrowingUnionWithBang.js b/tests/baselines/reference/narrowingUnionWithBang.js new file mode 100644 index 0000000000000..5237ced271dce --- /dev/null +++ b/tests/baselines/reference/narrowingUnionWithBang.js @@ -0,0 +1,85 @@ +//// [narrowingUnionWithBang.ts] +type WorkingType = { + thing?: + { name: 'Error1', message: string } | + { name: 'Error2', message: string } | + { name: 'Error3', message: string } | + { name: 'Error4', message: string } | + { name: 'Error5', message: string } | + { name: 'Error6', message: string } | + { name: 'Error7', message: string } | + { name: 'Error8', message: string } | + { name: 'Error9', message: string } | + { name: 'Correct', id: string } +}; +const working: WorkingType = null as unknown as WorkingType; +if (working.thing!.name !== "Correct") { + console.log(working.thing!.message) +} else { + console.log(working.thing!.id); +} + +type BorkedType = { + thing?: + { name: 'Error1', message: string } | + { name: 'Error2', message: string } | + { name: 'Error3', message: string } | + { name: 'Error4', message: string } | + { name: 'Error5', message: string } | + { name: 'Error6', message: string } | + { name: 'Error7', message: string } | + { name: 'Error8', message: string } | + { name: 'Correct', id: string } +}; +const borked: BorkedType = null as unknown as BorkedType; +if (borked.thing!.name !== "Correct") { + console.log(borked.thing!.message); // Error: We don't narrow it to exclude the "correct" variant here +} else { + console.log(borked.thing!.id); // Error: We also don't narrow it here +} + +export type FixedType = { + thing?: + { name: 'Error1', message: string } | + { name: 'Error2', message: string } | + { name: 'Error3', message: string } | + { name: 'Error4', message: string } | + { name: 'Error5', message: string } | + { name: 'Error6', message: string } | + { name: 'Error7', message: string } | + { name: 'Error8', message: string } | + { name: 'Correct', id: string } +}; +const fixed: FixedType = null as unknown as FixedType; + +if (fixed.thing?.name !== "Correct") { + console.log(fixed.thing!.message); +} else { + console.log(fixed.thing.id); +} + +//// [narrowingUnionWithBang.js] +"use strict"; +var _a; +Object.defineProperty(exports, "__esModule", { value: true }); +var working = null; +if (working.thing.name !== "Correct") { + console.log(working.thing.message); +} +else { + console.log(working.thing.id); +} +var borked = null; +if (borked.thing.name !== "Correct") { + console.log(borked.thing.message); // Error: We don't narrow it to exclude the "correct" variant here +} +else { + console.log(borked.thing.id); // Error: We also don't narrow it here +} +var fixed = null; +if (((_a = fixed.thing) === null || _a === void 0 ? void 0 : _a.name) !== "Correct") { + console.log(fixed.thing.message); +} +else { + console.log(fixed.thing.id); +} diff --git a/tests/baselines/reference/narrowingUnionWithBang.symbols b/tests/baselines/reference/narrowingUnionWithBang.symbols new file mode 100644 index 0000000000000..bc486042e3e99 --- /dev/null +++ b/tests/baselines/reference/narrowingUnionWithBang.symbols @@ -0,0 +1,235 @@ +=== tests/cases/compiler/narrowingUnionWithBang.ts === +type WorkingType = { +>WorkingType : Symbol(WorkingType, Decl(narrowingUnionWithBang.ts, 0, 0)) + + thing?: +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 0, 20)) + + { name: 'Error1', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 2, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 2, 21)) + + { name: 'Error2', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 3, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 3, 21)) + + { name: 'Error3', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 4, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 4, 21)) + + { name: 'Error4', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 5, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 5, 21)) + + { name: 'Error5', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 6, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 6, 21)) + + { name: 'Error6', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 7, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 7, 21)) + + { name: 'Error7', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 8, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 8, 21)) + + { name: 'Error8', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 9, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 9, 21)) + + { name: 'Error9', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 10, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 10, 21)) + + { name: 'Correct', id: string } +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 11, 5)) +>id : Symbol(id, Decl(narrowingUnionWithBang.ts, 11, 22)) + +}; +const working: WorkingType = null as unknown as WorkingType; +>working : Symbol(working, Decl(narrowingUnionWithBang.ts, 13, 5)) +>WorkingType : Symbol(WorkingType, Decl(narrowingUnionWithBang.ts, 0, 0)) +>WorkingType : Symbol(WorkingType, Decl(narrowingUnionWithBang.ts, 0, 0)) + +if (working.thing!.name !== "Correct") { +>working.thing!.name : Symbol(name, Decl(narrowingUnionWithBang.ts, 2, 5), Decl(narrowingUnionWithBang.ts, 3, 5), Decl(narrowingUnionWithBang.ts, 4, 5), Decl(narrowingUnionWithBang.ts, 5, 5), Decl(narrowingUnionWithBang.ts, 6, 5) ... and 5 more) +>working.thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 0, 20)) +>working : Symbol(working, Decl(narrowingUnionWithBang.ts, 13, 5)) +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 0, 20)) +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 2, 5), Decl(narrowingUnionWithBang.ts, 3, 5), Decl(narrowingUnionWithBang.ts, 4, 5), Decl(narrowingUnionWithBang.ts, 5, 5), Decl(narrowingUnionWithBang.ts, 6, 5) ... and 5 more) + + console.log(working.thing!.message) +>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, --, --)) +>working.thing!.message : Symbol(message, Decl(narrowingUnionWithBang.ts, 2, 21), Decl(narrowingUnionWithBang.ts, 3, 21), Decl(narrowingUnionWithBang.ts, 4, 21), Decl(narrowingUnionWithBang.ts, 5, 21), Decl(narrowingUnionWithBang.ts, 6, 21) ... and 4 more) +>working.thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 0, 20)) +>working : Symbol(working, Decl(narrowingUnionWithBang.ts, 13, 5)) +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 0, 20)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 2, 21), Decl(narrowingUnionWithBang.ts, 3, 21), Decl(narrowingUnionWithBang.ts, 4, 21), Decl(narrowingUnionWithBang.ts, 5, 21), Decl(narrowingUnionWithBang.ts, 6, 21) ... and 4 more) + +} else { + console.log(working.thing!.id); +>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, --, --)) +>working.thing!.id : Symbol(id, Decl(narrowingUnionWithBang.ts, 11, 22)) +>working.thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 0, 20)) +>working : Symbol(working, Decl(narrowingUnionWithBang.ts, 13, 5)) +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 0, 20)) +>id : Symbol(id, Decl(narrowingUnionWithBang.ts, 11, 22)) +} + +type BorkedType = { +>BorkedType : Symbol(BorkedType, Decl(narrowingUnionWithBang.ts, 18, 1)) + + thing?: +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 20, 19)) + + { name: 'Error1', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 22, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 22, 21)) + + { name: 'Error2', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 23, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 23, 21)) + + { name: 'Error3', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 24, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 24, 21)) + + { name: 'Error4', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 25, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 25, 21)) + + { name: 'Error5', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 26, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 26, 21)) + + { name: 'Error6', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 27, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 27, 21)) + + { name: 'Error7', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 28, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 28, 21)) + + { name: 'Error8', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 29, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 29, 21)) + + { name: 'Correct', id: string } +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 30, 5)) +>id : Symbol(id, Decl(narrowingUnionWithBang.ts, 30, 22)) + +}; +const borked: BorkedType = null as unknown as BorkedType; +>borked : Symbol(borked, Decl(narrowingUnionWithBang.ts, 32, 5)) +>BorkedType : Symbol(BorkedType, Decl(narrowingUnionWithBang.ts, 18, 1)) +>BorkedType : Symbol(BorkedType, Decl(narrowingUnionWithBang.ts, 18, 1)) + +if (borked.thing!.name !== "Correct") { +>borked.thing!.name : Symbol(name, Decl(narrowingUnionWithBang.ts, 22, 5), Decl(narrowingUnionWithBang.ts, 23, 5), Decl(narrowingUnionWithBang.ts, 24, 5), Decl(narrowingUnionWithBang.ts, 25, 5), Decl(narrowingUnionWithBang.ts, 26, 5) ... and 4 more) +>borked.thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 20, 19)) +>borked : Symbol(borked, Decl(narrowingUnionWithBang.ts, 32, 5)) +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 20, 19)) +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 22, 5), Decl(narrowingUnionWithBang.ts, 23, 5), Decl(narrowingUnionWithBang.ts, 24, 5), Decl(narrowingUnionWithBang.ts, 25, 5), Decl(narrowingUnionWithBang.ts, 26, 5) ... and 4 more) + + console.log(borked.thing!.message); // Error: We don't narrow it to exclude the "correct" variant here +>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, --, --)) +>borked.thing!.message : Symbol(message, Decl(narrowingUnionWithBang.ts, 22, 21), Decl(narrowingUnionWithBang.ts, 23, 21), Decl(narrowingUnionWithBang.ts, 24, 21), Decl(narrowingUnionWithBang.ts, 25, 21), Decl(narrowingUnionWithBang.ts, 26, 21) ... and 3 more) +>borked.thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 20, 19)) +>borked : Symbol(borked, Decl(narrowingUnionWithBang.ts, 32, 5)) +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 20, 19)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 22, 21), Decl(narrowingUnionWithBang.ts, 23, 21), Decl(narrowingUnionWithBang.ts, 24, 21), Decl(narrowingUnionWithBang.ts, 25, 21), Decl(narrowingUnionWithBang.ts, 26, 21) ... and 3 more) + +} else { + console.log(borked.thing!.id); // Error: We also don't narrow it here +>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, --, --)) +>borked.thing!.id : Symbol(id, Decl(narrowingUnionWithBang.ts, 30, 22)) +>borked.thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 20, 19)) +>borked : Symbol(borked, Decl(narrowingUnionWithBang.ts, 32, 5)) +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 20, 19)) +>id : Symbol(id, Decl(narrowingUnionWithBang.ts, 30, 22)) +} + +export type FixedType = { +>FixedType : Symbol(FixedType, Decl(narrowingUnionWithBang.ts, 37, 1)) + + thing?: +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 39, 25)) + + { name: 'Error1', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 41, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 41, 21)) + + { name: 'Error2', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 42, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 42, 21)) + + { name: 'Error3', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 43, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 43, 21)) + + { name: 'Error4', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 44, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 44, 21)) + + { name: 'Error5', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 45, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 45, 21)) + + { name: 'Error6', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 46, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 46, 21)) + + { name: 'Error7', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 47, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 47, 21)) + + { name: 'Error8', message: string } | +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 48, 5)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 48, 21)) + + { name: 'Correct', id: string } +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 49, 5)) +>id : Symbol(id, Decl(narrowingUnionWithBang.ts, 49, 22)) + +}; +const fixed: FixedType = null as unknown as FixedType; +>fixed : Symbol(fixed, Decl(narrowingUnionWithBang.ts, 51, 5)) +>FixedType : Symbol(FixedType, Decl(narrowingUnionWithBang.ts, 37, 1)) +>FixedType : Symbol(FixedType, Decl(narrowingUnionWithBang.ts, 37, 1)) + +if (fixed.thing?.name !== "Correct") { +>fixed.thing?.name : Symbol(name, Decl(narrowingUnionWithBang.ts, 41, 5), Decl(narrowingUnionWithBang.ts, 42, 5), Decl(narrowingUnionWithBang.ts, 43, 5), Decl(narrowingUnionWithBang.ts, 44, 5), Decl(narrowingUnionWithBang.ts, 45, 5) ... and 4 more) +>fixed.thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 39, 25)) +>fixed : Symbol(fixed, Decl(narrowingUnionWithBang.ts, 51, 5)) +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 39, 25)) +>name : Symbol(name, Decl(narrowingUnionWithBang.ts, 41, 5), Decl(narrowingUnionWithBang.ts, 42, 5), Decl(narrowingUnionWithBang.ts, 43, 5), Decl(narrowingUnionWithBang.ts, 44, 5), Decl(narrowingUnionWithBang.ts, 45, 5) ... and 4 more) + + console.log(fixed.thing!.message); +>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, --, --)) +>fixed.thing!.message : Symbol(message, Decl(narrowingUnionWithBang.ts, 41, 21), Decl(narrowingUnionWithBang.ts, 42, 21), Decl(narrowingUnionWithBang.ts, 43, 21), Decl(narrowingUnionWithBang.ts, 44, 21), Decl(narrowingUnionWithBang.ts, 45, 21) ... and 3 more) +>fixed.thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 39, 25)) +>fixed : Symbol(fixed, Decl(narrowingUnionWithBang.ts, 51, 5)) +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 39, 25)) +>message : Symbol(message, Decl(narrowingUnionWithBang.ts, 41, 21), Decl(narrowingUnionWithBang.ts, 42, 21), Decl(narrowingUnionWithBang.ts, 43, 21), Decl(narrowingUnionWithBang.ts, 44, 21), Decl(narrowingUnionWithBang.ts, 45, 21) ... and 3 more) + +} else { + console.log(fixed.thing.id); +>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, --, --)) +>fixed.thing.id : Symbol(id, Decl(narrowingUnionWithBang.ts, 49, 22)) +>fixed.thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 39, 25)) +>fixed : Symbol(fixed, Decl(narrowingUnionWithBang.ts, 51, 5)) +>thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 39, 25)) +>id : Symbol(id, Decl(narrowingUnionWithBang.ts, 49, 22)) +} diff --git a/tests/baselines/reference/narrowingUnionWithBang.types b/tests/baselines/reference/narrowingUnionWithBang.types new file mode 100644 index 0000000000000..4090bcf2f8b13 --- /dev/null +++ b/tests/baselines/reference/narrowingUnionWithBang.types @@ -0,0 +1,257 @@ +=== tests/cases/compiler/narrowingUnionWithBang.ts === +type WorkingType = { +>WorkingType : { thing?: { name: 'Error1'; message: string; } | { name: 'Error2'; message: string; } | { name: 'Error3'; message: string; } | { name: 'Error4'; message: string; } | { name: 'Error5'; message: string; } | { name: 'Error6'; message: string; } | { name: 'Error7'; message: string; } | { name: 'Error8'; message: string; } | { name: 'Error9'; message: string; } | { name: 'Correct'; id: string; } | undefined; } + + thing?: +>thing : { name: 'Error1'; message: string; } | { name: 'Error2'; message: string; } | { name: 'Error3'; message: string; } | { name: 'Error4'; message: string; } | { name: 'Error5'; message: string; } | { name: 'Error6'; message: string; } | { name: 'Error7'; message: string; } | { name: 'Error8'; message: string; } | { name: 'Error9'; message: string; } | { name: 'Correct'; id: string; } | undefined + + { name: 'Error1', message: string } | +>name : "Error1" +>message : string + + { name: 'Error2', message: string } | +>name : "Error2" +>message : string + + { name: 'Error3', message: string } | +>name : "Error3" +>message : string + + { name: 'Error4', message: string } | +>name : "Error4" +>message : string + + { name: 'Error5', message: string } | +>name : "Error5" +>message : string + + { name: 'Error6', message: string } | +>name : "Error6" +>message : string + + { name: 'Error7', message: string } | +>name : "Error7" +>message : string + + { name: 'Error8', message: string } | +>name : "Error8" +>message : string + + { name: 'Error9', message: string } | +>name : "Error9" +>message : string + + { name: 'Correct', id: string } +>name : "Correct" +>id : string + +}; +const working: WorkingType = null as unknown as WorkingType; +>working : WorkingType +>null as unknown as WorkingType : WorkingType +>null as unknown : unknown +>null : null + +if (working.thing!.name !== "Correct") { +>working.thing!.name !== "Correct" : boolean +>working.thing!.name : "Error1" | "Error2" | "Error3" | "Error4" | "Error5" | "Error6" | "Error7" | "Error8" | "Error9" | "Correct" +>working.thing! : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Error9"; message: string; } | { name: "Correct"; id: string; } +>working.thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Error9"; message: string; } | { name: "Correct"; id: string; } | undefined +>working : WorkingType +>thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Error9"; message: string; } | { name: "Correct"; id: string; } | undefined +>name : "Error1" | "Error2" | "Error3" | "Error4" | "Error5" | "Error6" | "Error7" | "Error8" | "Error9" | "Correct" +>"Correct" : "Correct" + + console.log(working.thing!.message) +>console.log(working.thing!.message) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>working.thing!.message : string +>working.thing! : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Error9"; message: string; } +>working.thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Error9"; message: string; } | undefined +>working : WorkingType +>thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Error9"; message: string; } | undefined +>message : string + +} else { + console.log(working.thing!.id); +>console.log(working.thing!.id) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>working.thing!.id : string +>working.thing! : { name: "Correct"; id: string; } +>working.thing : { name: "Correct"; id: string; } +>working : WorkingType +>thing : { name: "Correct"; id: string; } +>id : string +} + +type BorkedType = { +>BorkedType : { thing?: { name: 'Error1'; message: string; } | { name: 'Error2'; message: string; } | { name: 'Error3'; message: string; } | { name: 'Error4'; message: string; } | { name: 'Error5'; message: string; } | { name: 'Error6'; message: string; } | { name: 'Error7'; message: string; } | { name: 'Error8'; message: string; } | { name: 'Correct'; id: string; } | undefined; } + + thing?: +>thing : { name: 'Error1'; message: string; } | { name: 'Error2'; message: string; } | { name: 'Error3'; message: string; } | { name: 'Error4'; message: string; } | { name: 'Error5'; message: string; } | { name: 'Error6'; message: string; } | { name: 'Error7'; message: string; } | { name: 'Error8'; message: string; } | { name: 'Correct'; id: string; } | undefined + + { name: 'Error1', message: string } | +>name : "Error1" +>message : string + + { name: 'Error2', message: string } | +>name : "Error2" +>message : string + + { name: 'Error3', message: string } | +>name : "Error3" +>message : string + + { name: 'Error4', message: string } | +>name : "Error4" +>message : string + + { name: 'Error5', message: string } | +>name : "Error5" +>message : string + + { name: 'Error6', message: string } | +>name : "Error6" +>message : string + + { name: 'Error7', message: string } | +>name : "Error7" +>message : string + + { name: 'Error8', message: string } | +>name : "Error8" +>message : string + + { name: 'Correct', id: string } +>name : "Correct" +>id : string + +}; +const borked: BorkedType = null as unknown as BorkedType; +>borked : BorkedType +>null as unknown as BorkedType : BorkedType +>null as unknown : unknown +>null : null + +if (borked.thing!.name !== "Correct") { +>borked.thing!.name !== "Correct" : boolean +>borked.thing!.name : "Error1" | "Error2" | "Error3" | "Error4" | "Error5" | "Error6" | "Error7" | "Error8" | "Correct" +>borked.thing! : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Correct"; id: string; } +>borked.thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Correct"; id: string; } | undefined +>borked : BorkedType +>thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Correct"; id: string; } | undefined +>name : "Error1" | "Error2" | "Error3" | "Error4" | "Error5" | "Error6" | "Error7" | "Error8" | "Correct" +>"Correct" : "Correct" + + console.log(borked.thing!.message); // Error: We don't narrow it to exclude the "correct" variant here +>console.log(borked.thing!.message) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>borked.thing!.message : string +>borked.thing! : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } +>borked.thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | undefined +>borked : BorkedType +>thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | undefined +>message : string + +} else { + console.log(borked.thing!.id); // Error: We also don't narrow it here +>console.log(borked.thing!.id) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>borked.thing!.id : string +>borked.thing! : { name: "Correct"; id: string; } +>borked.thing : { name: "Correct"; id: string; } | undefined +>borked : BorkedType +>thing : { name: "Correct"; id: string; } | undefined +>id : string +} + +export type FixedType = { +>FixedType : { thing?: { name: 'Error1'; message: string; } | { name: 'Error2'; message: string; } | { name: 'Error3'; message: string; } | { name: 'Error4'; message: string; } | { name: 'Error5'; message: string; } | { name: 'Error6'; message: string; } | { name: 'Error7'; message: string; } | { name: 'Error8'; message: string; } | { name: 'Correct'; id: string; } | undefined; } + + thing?: +>thing : { name: 'Error1'; message: string; } | { name: 'Error2'; message: string; } | { name: 'Error3'; message: string; } | { name: 'Error4'; message: string; } | { name: 'Error5'; message: string; } | { name: 'Error6'; message: string; } | { name: 'Error7'; message: string; } | { name: 'Error8'; message: string; } | { name: 'Correct'; id: string; } | undefined + + { name: 'Error1', message: string } | +>name : "Error1" +>message : string + + { name: 'Error2', message: string } | +>name : "Error2" +>message : string + + { name: 'Error3', message: string } | +>name : "Error3" +>message : string + + { name: 'Error4', message: string } | +>name : "Error4" +>message : string + + { name: 'Error5', message: string } | +>name : "Error5" +>message : string + + { name: 'Error6', message: string } | +>name : "Error6" +>message : string + + { name: 'Error7', message: string } | +>name : "Error7" +>message : string + + { name: 'Error8', message: string } | +>name : "Error8" +>message : string + + { name: 'Correct', id: string } +>name : "Correct" +>id : string + +}; +const fixed: FixedType = null as unknown as FixedType; +>fixed : FixedType +>null as unknown as FixedType : FixedType +>null as unknown : unknown +>null : null + +if (fixed.thing?.name !== "Correct") { +>fixed.thing?.name !== "Correct" : boolean +>fixed.thing?.name : "Error1" | "Error2" | "Error3" | "Error4" | "Error5" | "Error6" | "Error7" | "Error8" | "Correct" | undefined +>fixed.thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Correct"; id: string; } | undefined +>fixed : FixedType +>thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | { name: "Correct"; id: string; } | undefined +>name : "Error1" | "Error2" | "Error3" | "Error4" | "Error5" | "Error6" | "Error7" | "Error8" | "Correct" | undefined +>"Correct" : "Correct" + + console.log(fixed.thing!.message); +>console.log(fixed.thing!.message) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>fixed.thing!.message : string +>fixed.thing! : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } +>fixed.thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | undefined +>fixed : FixedType +>thing : { name: "Error1"; message: string; } | { name: "Error2"; message: string; } | { name: "Error3"; message: string; } | { name: "Error4"; message: string; } | { name: "Error5"; message: string; } | { name: "Error6"; message: string; } | { name: "Error7"; message: string; } | { name: "Error8"; message: string; } | undefined +>message : string + +} else { + console.log(fixed.thing.id); +>console.log(fixed.thing.id) : void +>console.log : (...data: any[]) => void +>console : Console +>log : (...data: any[]) => void +>fixed.thing.id : string +>fixed.thing : { name: "Correct"; id: string; } +>fixed : FixedType +>thing : { name: "Correct"; id: string; } +>id : string +} diff --git a/tests/cases/compiler/narrowingUnionWithBang.ts b/tests/cases/compiler/narrowingUnionWithBang.ts new file mode 100644 index 0000000000000..68eeb382d78bd --- /dev/null +++ b/tests/cases/compiler/narrowingUnionWithBang.ts @@ -0,0 +1,59 @@ +// @strict: true +type WorkingType = { + thing?: + { name: 'Error1', message: string } | + { name: 'Error2', message: string } | + { name: 'Error3', message: string } | + { name: 'Error4', message: string } | + { name: 'Error5', message: string } | + { name: 'Error6', message: string } | + { name: 'Error7', message: string } | + { name: 'Error8', message: string } | + { name: 'Error9', message: string } | + { name: 'Correct', id: string } +}; +const working: WorkingType = null as unknown as WorkingType; +if (working.thing!.name !== "Correct") { + console.log(working.thing!.message) +} else { + console.log(working.thing!.id); +} + +type BorkedType = { + thing?: + { name: 'Error1', message: string } | + { name: 'Error2', message: string } | + { name: 'Error3', message: string } | + { name: 'Error4', message: string } | + { name: 'Error5', message: string } | + { name: 'Error6', message: string } | + { name: 'Error7', message: string } | + { name: 'Error8', message: string } | + { name: 'Correct', id: string } +}; +const borked: BorkedType = null as unknown as BorkedType; +if (borked.thing!.name !== "Correct") { + console.log(borked.thing!.message); // Error: We don't narrow it to exclude the "correct" variant here +} else { + console.log(borked.thing!.id); // Error: We also don't narrow it here +} + +export type FixedType = { + thing?: + { name: 'Error1', message: string } | + { name: 'Error2', message: string } | + { name: 'Error3', message: string } | + { name: 'Error4', message: string } | + { name: 'Error5', message: string } | + { name: 'Error6', message: string } | + { name: 'Error7', message: string } | + { name: 'Error8', message: string } | + { name: 'Correct', id: string } +}; +const fixed: FixedType = null as unknown as FixedType; + +if (fixed.thing?.name !== "Correct") { + console.log(fixed.thing!.message); +} else { + console.log(fixed.thing.id); +} \ No newline at end of file From 4f4def5188e42b33b0325c76a43a6966eea4c8aa Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 6 Jan 2023 16:22:10 -0800 Subject: [PATCH 2/3] remove comment from test --- tests/baselines/reference/narrowingUnionWithBang.js | 8 ++++---- tests/baselines/reference/narrowingUnionWithBang.symbols | 4 ++-- tests/baselines/reference/narrowingUnionWithBang.types | 4 ++-- tests/cases/compiler/narrowingUnionWithBang.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/baselines/reference/narrowingUnionWithBang.js b/tests/baselines/reference/narrowingUnionWithBang.js index 5237ced271dce..09b1c234f12a5 100644 --- a/tests/baselines/reference/narrowingUnionWithBang.js +++ b/tests/baselines/reference/narrowingUnionWithBang.js @@ -33,9 +33,9 @@ type BorkedType = { }; const borked: BorkedType = null as unknown as BorkedType; if (borked.thing!.name !== "Correct") { - console.log(borked.thing!.message); // Error: We don't narrow it to exclude the "correct" variant here + console.log(borked.thing!.message); } else { - console.log(borked.thing!.id); // Error: We also don't narrow it here + console.log(borked.thing!.id); } export type FixedType = { @@ -71,10 +71,10 @@ else { } var borked = null; if (borked.thing.name !== "Correct") { - console.log(borked.thing.message); // Error: We don't narrow it to exclude the "correct" variant here + console.log(borked.thing.message); } else { - console.log(borked.thing.id); // Error: We also don't narrow it here + console.log(borked.thing.id); } var fixed = null; if (((_a = fixed.thing) === null || _a === void 0 ? void 0 : _a.name) !== "Correct") { diff --git a/tests/baselines/reference/narrowingUnionWithBang.symbols b/tests/baselines/reference/narrowingUnionWithBang.symbols index bc486042e3e99..6bb48ad84f934 100644 --- a/tests/baselines/reference/narrowingUnionWithBang.symbols +++ b/tests/baselines/reference/narrowingUnionWithBang.symbols @@ -135,7 +135,7 @@ if (borked.thing!.name !== "Correct") { >thing : Symbol(thing, Decl(narrowingUnionWithBang.ts, 20, 19)) >name : Symbol(name, Decl(narrowingUnionWithBang.ts, 22, 5), Decl(narrowingUnionWithBang.ts, 23, 5), Decl(narrowingUnionWithBang.ts, 24, 5), Decl(narrowingUnionWithBang.ts, 25, 5), Decl(narrowingUnionWithBang.ts, 26, 5) ... and 4 more) - console.log(borked.thing!.message); // Error: We don't narrow it to exclude the "correct" variant here + console.log(borked.thing!.message); >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, --, --)) @@ -146,7 +146,7 @@ if (borked.thing!.name !== "Correct") { >message : Symbol(message, Decl(narrowingUnionWithBang.ts, 22, 21), Decl(narrowingUnionWithBang.ts, 23, 21), Decl(narrowingUnionWithBang.ts, 24, 21), Decl(narrowingUnionWithBang.ts, 25, 21), Decl(narrowingUnionWithBang.ts, 26, 21) ... and 3 more) } else { - console.log(borked.thing!.id); // Error: We also don't narrow it here + console.log(borked.thing!.id); >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, --, --)) diff --git a/tests/baselines/reference/narrowingUnionWithBang.types b/tests/baselines/reference/narrowingUnionWithBang.types index 4090bcf2f8b13..431fbb48c97e4 100644 --- a/tests/baselines/reference/narrowingUnionWithBang.types +++ b/tests/baselines/reference/narrowingUnionWithBang.types @@ -147,7 +147,7 @@ if (borked.thing!.name !== "Correct") { >name : "Error1" | "Error2" | "Error3" | "Error4" | "Error5" | "Error6" | "Error7" | "Error8" | "Correct" >"Correct" : "Correct" - console.log(borked.thing!.message); // Error: We don't narrow it to exclude the "correct" variant here + console.log(borked.thing!.message); >console.log(borked.thing!.message) : void >console.log : (...data: any[]) => void >console : Console @@ -160,7 +160,7 @@ if (borked.thing!.name !== "Correct") { >message : string } else { - console.log(borked.thing!.id); // Error: We also don't narrow it here + console.log(borked.thing!.id); >console.log(borked.thing!.id) : void >console.log : (...data: any[]) => void >console : Console diff --git a/tests/cases/compiler/narrowingUnionWithBang.ts b/tests/cases/compiler/narrowingUnionWithBang.ts index 68eeb382d78bd..21e0bc36695b6 100644 --- a/tests/cases/compiler/narrowingUnionWithBang.ts +++ b/tests/cases/compiler/narrowingUnionWithBang.ts @@ -33,9 +33,9 @@ type BorkedType = { }; const borked: BorkedType = null as unknown as BorkedType; if (borked.thing!.name !== "Correct") { - console.log(borked.thing!.message); // Error: We don't narrow it to exclude the "correct" variant here + console.log(borked.thing!.message); } else { - console.log(borked.thing!.id); // Error: We also don't narrow it here + console.log(borked.thing!.id); } export type FixedType = { From dca634c958939309b9efc0b5807246f06cb9c64f Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 9 Jan 2023 12:46:18 -0800 Subject: [PATCH 3/3] don't make prop type optional for non null expression access --- src/compiler/checker.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ff1b2e845bf64..99ffe1c9eb23f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -26300,12 +26300,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (propName === undefined) { return type; } - const removeNullable = strictNullChecks && (isOptionalChain(access) || isNonNullAccess(access)) && maybeTypeOfKind(type, TypeFlags.Nullable); + const optionalChain = isOptionalChain(access); + const removeNullable = strictNullChecks && (optionalChain || isNonNullAccess(access)) && maybeTypeOfKind(type, TypeFlags.Nullable); let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName); if (!propType) { return type; } - propType = removeNullable ? getOptionalType(propType) : propType; + propType = removeNullable && optionalChain ? getOptionalType(propType) : propType; const narrowedPropType = narrowType(propType); return filterType(type, t => { const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName);