diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c31732cc24ac9..06395d935b7df 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20573,10 +20573,15 @@ namespace ts { } propType = getConstraintForLocation(getTypeOfSymbol(prop), node); } + return getFlowTypeOfAccessExpression(node, prop, propType, right); + } + + function getFlowTypeOfAccessExpression(node: ElementAccessExpression | PropertyAccessExpression | QualifiedName, prop: Symbol | undefined, propType: Type, errorNode: Node) { // Only compute control flow type if this is a property access expression that isn't an // assignment target, and the referenced property was declared as a variable, property, // accessor, or optional method. - if (node.kind !== SyntaxKind.PropertyAccessExpression || + const assignmentKind = getAssignmentTargetKind(node); + if (node.kind !== SyntaxKind.ElementAccessExpression && node.kind !== SyntaxKind.PropertyAccessExpression || assignmentKind === AssignmentKind.Definite || prop && !(prop.flags & (SymbolFlags.Variable | SymbolFlags.Property | SymbolFlags.Accessor)) && !(prop.flags & SymbolFlags.Method && propType.flags & TypeFlags.Union)) { return propType; @@ -20586,7 +20591,7 @@ namespace ts { // and if we are in a constructor of the same class as the property declaration, assume that // the property is uninitialized at the top of the control flow. let assumeUninitialized = false; - if (strictNullChecks && strictPropertyInitialization && left.kind === SyntaxKind.ThisKeyword) { + if (strictNullChecks && strictPropertyInitialization && node.expression.kind === SyntaxKind.ThisKeyword) { const declaration = prop && prop.valueDeclaration; if (declaration && isInstancePropertyWithoutInitializer(declaration)) { const flowContainer = getControlFlowContainer(node); @@ -20603,7 +20608,7 @@ namespace ts { } const flowType = getFlowTypeOfReference(node, propType, assumeUninitialized ? getOptionalType(propType) : propType); if (assumeUninitialized && !(getFalsyFlags(propType) & TypeFlags.Undefined) && getFalsyFlags(flowType) & TypeFlags.Undefined) { - error(right, Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(prop!)); // TODO: GH#18217 + error(errorNode, Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(prop!)); // TODO: GH#18217 // Return the declared type to reduce follow-on errors return propType; } @@ -20960,7 +20965,7 @@ namespace ts { AccessFlags.Writing | (isGenericObjectType(objectType) && !isThisTypeParameter(objectType) ? AccessFlags.NoIndexSignatures : 0) : AccessFlags.None; const indexedAccessType = getIndexedAccessTypeOrUndefined(objectType, effectiveIndexType, node, accessFlags) || errorType; - return checkIndexedAccessIndexType(indexedAccessType, node); + return checkIndexedAccessIndexType(getFlowTypeOfAccessExpression(node, indexedAccessType.symbol, indexedAccessType, indexExpression), node); } function checkThatExpressionIsProperSymbolReference(expression: Expression, expressionType: Type, reportError: boolean): boolean { diff --git a/tests/baselines/reference/controlFlowElementAccess2.js b/tests/baselines/reference/controlFlowElementAccess2.js new file mode 100644 index 0000000000000..050fc15b8af1f --- /dev/null +++ b/tests/baselines/reference/controlFlowElementAccess2.js @@ -0,0 +1,25 @@ +//// [controlFlowElementAccess2.ts] +declare const config: { + [key: string]: boolean | { prop: string }; +}; + +if (typeof config['works'] !== 'boolean') { + config.works.prop = 'test'; // ok + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } +} +if (typeof config.works !== 'boolean') { + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } + config.works.prop = 'test'; // ok +} + + +//// [controlFlowElementAccess2.js] +"use strict"; +if (typeof config['works'] !== 'boolean') { + config.works.prop = 'test'; // ok + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } +} +if (typeof config.works !== 'boolean') { + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } + config.works.prop = 'test'; // ok +} diff --git a/tests/baselines/reference/controlFlowElementAccess2.symbols b/tests/baselines/reference/controlFlowElementAccess2.symbols new file mode 100644 index 0000000000000..5cdd890a75973 --- /dev/null +++ b/tests/baselines/reference/controlFlowElementAccess2.symbols @@ -0,0 +1,37 @@ +=== tests/cases/conformance/controlFlow/controlFlowElementAccess2.ts === +declare const config: { +>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13)) + + [key: string]: boolean | { prop: string }; +>key : Symbol(key, Decl(controlFlowElementAccess2.ts, 1, 5)) +>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30)) + +}; + +if (typeof config['works'] !== 'boolean') { +>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13)) + + config.works.prop = 'test'; // ok +>config.works.prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30)) +>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13)) +>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30)) + + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } +>config['works'].prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30)) +>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13)) +>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30)) +} +if (typeof config.works !== 'boolean') { +>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13)) + + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } +>config['works'].prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30)) +>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13)) +>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30)) + + config.works.prop = 'test'; // ok +>config.works.prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30)) +>config : Symbol(config, Decl(controlFlowElementAccess2.ts, 0, 13)) +>prop : Symbol(prop, Decl(controlFlowElementAccess2.ts, 1, 30)) +} + diff --git a/tests/baselines/reference/controlFlowElementAccess2.types b/tests/baselines/reference/controlFlowElementAccess2.types new file mode 100644 index 0000000000000..65dca4de50226 --- /dev/null +++ b/tests/baselines/reference/controlFlowElementAccess2.types @@ -0,0 +1,63 @@ +=== tests/cases/conformance/controlFlow/controlFlowElementAccess2.ts === +declare const config: { +>config : { [key: string]: boolean | { prop: string; }; } + + [key: string]: boolean | { prop: string }; +>key : string +>prop : string + +}; + +if (typeof config['works'] !== 'boolean') { +>typeof config['works'] !== 'boolean' : boolean +>typeof config['works'] : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>config['works'] : boolean | { prop: string; } +>config : { [key: string]: boolean | { prop: string; }; } +>'works' : "works" +>'boolean' : "boolean" + + config.works.prop = 'test'; // ok +>config.works.prop = 'test' : "test" +>config.works.prop : string +>config.works : { prop: string; } +>config : { [key: string]: boolean | { prop: string; }; } +>works : { prop: string; } +>prop : string +>'test' : "test" + + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } +>config['works'].prop = 'test' : "test" +>config['works'].prop : string +>config['works'] : { prop: string; } +>config : { [key: string]: boolean | { prop: string; }; } +>'works' : "works" +>prop : string +>'test' : "test" +} +if (typeof config.works !== 'boolean') { +>typeof config.works !== 'boolean' : boolean +>typeof config.works : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>config.works : boolean | { prop: string; } +>config : { [key: string]: boolean | { prop: string; }; } +>works : boolean | { prop: string; } +>'boolean' : "boolean" + + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } +>config['works'].prop = 'test' : "test" +>config['works'].prop : string +>config['works'] : { prop: string; } +>config : { [key: string]: boolean | { prop: string; }; } +>'works' : "works" +>prop : string +>'test' : "test" + + config.works.prop = 'test'; // ok +>config.works.prop = 'test' : "test" +>config.works.prop : string +>config.works : { prop: string; } +>config : { [key: string]: boolean | { prop: string; }; } +>works : { prop: string; } +>prop : string +>'test' : "test" +} + diff --git a/tests/cases/conformance/controlFlow/controlFlowElementAccess2.ts b/tests/cases/conformance/controlFlow/controlFlowElementAccess2.ts new file mode 100644 index 0000000000000..fa0592c973ee3 --- /dev/null +++ b/tests/cases/conformance/controlFlow/controlFlowElementAccess2.ts @@ -0,0 +1,13 @@ +// @strict: true +declare const config: { + [key: string]: boolean | { prop: string }; +}; + +if (typeof config['works'] !== 'boolean') { + config.works.prop = 'test'; // ok + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } +} +if (typeof config.works !== 'boolean') { + config['works'].prop = 'test'; // error, config['works']: boolean | { 'prop': string } + config.works.prop = 'test'; // ok +}