diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 07bb8f180f824..ff2360d57a011 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20219,19 +20219,25 @@ namespace ts { return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right); } + function isMethodAccessForCall(node: Node) { + while (node.parent.kind === SyntaxKind.ParenthesizedExpression) { + node = node.parent; + } + return isCallOrNewExpression(node.parent) && node.parent.expression === node; + } + function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier) { let propType: Type; const leftType = checkNonNullExpression(left); const parentSymbol = getNodeLinks(left).resolvedSymbol; - // We widen array literals to get type any[] instead of undefined[] in non-strict mode - const apparentType = getApparentType(isEmptyArrayLiteralType(leftType) ? getWidenedType(leftType) : leftType); + const assignmentKind = getAssignmentTargetKind(node); + const apparentType = getApparentType(assignmentKind !== AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(leftType) : leftType); if (isTypeAny(apparentType) || apparentType === silentNeverType) { if (isIdentifier(left) && parentSymbol) { markAliasReferenced(parentSymbol, node); } return apparentType; } - const assignmentKind = getAssignmentTargetKind(node); const prop = getPropertyOfType(apparentType, right.escapedText); if (isIdentifier(left) && parentSymbol && !(prop && isConstEnumOrConstEnumOnlyModule(prop))) { markAliasReferenced(parentSymbol, node); @@ -20625,7 +20631,8 @@ namespace ts { } function checkIndexedAccess(node: ElementAccessExpression): Type { - const objectType = checkNonNullExpression(node.expression); + const exprType = checkNonNullExpression(node.expression); + const objectType = getAssignmentTargetKind(node) !== AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(exprType) : exprType; const indexExpression = node.argumentExpression; if (!indexExpression) { diff --git a/tests/baselines/reference/propertyAccessWidening.errors.txt b/tests/baselines/reference/propertyAccessWidening.errors.txt new file mode 100644 index 0000000000000..5dc4762a540f1 --- /dev/null +++ b/tests/baselines/reference/propertyAccessWidening.errors.txt @@ -0,0 +1,34 @@ +tests/cases/conformance/expressions/propertyAccess/propertyAccessWidening.ts(18,21): error TS2339: Property 'a' does not exist on type '{ a: string; b: number; } | {}'. + Property 'a' does not exist on type '{}'. +tests/cases/conformance/expressions/propertyAccess/propertyAccessWidening.ts(19,5): error TS7053: Element implicitly has an 'any' type because expression of type '"a"' can't be used to index type '{}'. + Property 'a' does not exist on type '{}'. + + +==== tests/cases/conformance/expressions/propertyAccess/propertyAccessWidening.ts (2 errors) ==== + // Repro from #31762 + + function g1(headerNames: any) { + let t = [{ hasLineBreak: false, cells: [] }]; + const table = [{cells: headerNames }].concat(t); + } + + function g2(headerNames: any) { + let t = [{ hasLineBreak: false, cells: [] }]; + const table = [{cells: headerNames }]["concat"](t); + } + + // Object in property or element access is widened when target of assignment + + function foo(options?: { a: string, b: number }) { + let x1 = (options || {}).a; // Object type not widened + let x2 = (options || {})["a"]; // Object type not widened + (options || {}).a = 1; // Object type widened, error + ~ +!!! error TS2339: Property 'a' does not exist on type '{ a: string; b: number; } | {}'. +!!! error TS2339: Property 'a' does not exist on type '{}'. + (options || {})["a"] = 1; // Object type widened, error + ~~~~~~~~~~~~~~~~~~~~ +!!! error TS7053: Element implicitly has an 'any' type because expression of type '"a"' can't be used to index type '{}'. +!!! error TS7053: Property 'a' does not exist on type '{}'. + } + \ No newline at end of file diff --git a/tests/baselines/reference/propertyAccessWidening.js b/tests/baselines/reference/propertyAccessWidening.js new file mode 100644 index 0000000000000..e48b0ec3a6cb6 --- /dev/null +++ b/tests/baselines/reference/propertyAccessWidening.js @@ -0,0 +1,41 @@ +//// [propertyAccessWidening.ts] +// Repro from #31762 + +function g1(headerNames: any) { + let t = [{ hasLineBreak: false, cells: [] }]; + const table = [{cells: headerNames }].concat(t); +} + +function g2(headerNames: any) { + let t = [{ hasLineBreak: false, cells: [] }]; + const table = [{cells: headerNames }]["concat"](t); +} + +// Object in property or element access is widened when target of assignment + +function foo(options?: { a: string, b: number }) { + let x1 = (options || {}).a; // Object type not widened + let x2 = (options || {})["a"]; // Object type not widened + (options || {}).a = 1; // Object type widened, error + (options || {})["a"] = 1; // Object type widened, error +} + + +//// [propertyAccessWidening.js] +"use strict"; +// Repro from #31762 +function g1(headerNames) { + var t = [{ hasLineBreak: false, cells: [] }]; + var table = [{ cells: headerNames }].concat(t); +} +function g2(headerNames) { + var t = [{ hasLineBreak: false, cells: [] }]; + var table = [{ cells: headerNames }]["concat"](t); +} +// Object in property or element access is widened when target of assignment +function foo(options) { + var x1 = (options || {}).a; // Object type not widened + var x2 = (options || {})["a"]; // Object type not widened + (options || {}).a = 1; // Object type widened, error + (options || {})["a"] = 1; // Object type widened, error +} diff --git a/tests/baselines/reference/propertyAccessWidening.symbols b/tests/baselines/reference/propertyAccessWidening.symbols new file mode 100644 index 0000000000000..4e901d23dad29 --- /dev/null +++ b/tests/baselines/reference/propertyAccessWidening.symbols @@ -0,0 +1,65 @@ +=== tests/cases/conformance/expressions/propertyAccess/propertyAccessWidening.ts === +// Repro from #31762 + +function g1(headerNames: any) { +>g1 : Symbol(g1, Decl(propertyAccessWidening.ts, 0, 0)) +>headerNames : Symbol(headerNames, Decl(propertyAccessWidening.ts, 2, 12)) + + let t = [{ hasLineBreak: false, cells: [] }]; +>t : Symbol(t, Decl(propertyAccessWidening.ts, 3, 7)) +>hasLineBreak : Symbol(hasLineBreak, Decl(propertyAccessWidening.ts, 3, 14)) +>cells : Symbol(cells, Decl(propertyAccessWidening.ts, 3, 35)) + + const table = [{cells: headerNames }].concat(t); +>table : Symbol(table, Decl(propertyAccessWidening.ts, 4, 9)) +>[{cells: headerNames }].concat : Symbol(Array.concat, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>cells : Symbol(cells, Decl(propertyAccessWidening.ts, 4, 20)) +>headerNames : Symbol(headerNames, Decl(propertyAccessWidening.ts, 2, 12)) +>concat : Symbol(Array.concat, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>t : Symbol(t, Decl(propertyAccessWidening.ts, 3, 7)) +} + +function g2(headerNames: any) { +>g2 : Symbol(g2, Decl(propertyAccessWidening.ts, 5, 1)) +>headerNames : Symbol(headerNames, Decl(propertyAccessWidening.ts, 7, 12)) + + let t = [{ hasLineBreak: false, cells: [] }]; +>t : Symbol(t, Decl(propertyAccessWidening.ts, 8, 7)) +>hasLineBreak : Symbol(hasLineBreak, Decl(propertyAccessWidening.ts, 8, 14)) +>cells : Symbol(cells, Decl(propertyAccessWidening.ts, 8, 35)) + + const table = [{cells: headerNames }]["concat"](t); +>table : Symbol(table, Decl(propertyAccessWidening.ts, 9, 9)) +>cells : Symbol(cells, Decl(propertyAccessWidening.ts, 9, 20)) +>headerNames : Symbol(headerNames, Decl(propertyAccessWidening.ts, 7, 12)) +>"concat" : Symbol(Array.concat, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>t : Symbol(t, Decl(propertyAccessWidening.ts, 8, 7)) +} + +// Object in property or element access is widened when target of assignment + +function foo(options?: { a: string, b: number }) { +>foo : Symbol(foo, Decl(propertyAccessWidening.ts, 10, 1)) +>options : Symbol(options, Decl(propertyAccessWidening.ts, 14, 13)) +>a : Symbol(a, Decl(propertyAccessWidening.ts, 14, 24)) +>b : Symbol(b, Decl(propertyAccessWidening.ts, 14, 35)) + + let x1 = (options || {}).a; // Object type not widened +>x1 : Symbol(x1, Decl(propertyAccessWidening.ts, 15, 7)) +>(options || {}).a : Symbol(a, Decl(propertyAccessWidening.ts, 14, 24)) +>options : Symbol(options, Decl(propertyAccessWidening.ts, 14, 13)) +>a : Symbol(a, Decl(propertyAccessWidening.ts, 14, 24)) + + let x2 = (options || {})["a"]; // Object type not widened +>x2 : Symbol(x2, Decl(propertyAccessWidening.ts, 16, 7)) +>options : Symbol(options, Decl(propertyAccessWidening.ts, 14, 13)) +>"a" : Symbol(a, Decl(propertyAccessWidening.ts, 14, 24)) + + (options || {}).a = 1; // Object type widened, error +>options : Symbol(options, Decl(propertyAccessWidening.ts, 14, 13)) + + (options || {})["a"] = 1; // Object type widened, error +>options : Symbol(options, Decl(propertyAccessWidening.ts, 14, 13)) +>"a" : Symbol(a, Decl(propertyAccessWidening.ts, 14, 24)) +} + diff --git a/tests/baselines/reference/propertyAccessWidening.types b/tests/baselines/reference/propertyAccessWidening.types new file mode 100644 index 0000000000000..244b43be69fce --- /dev/null +++ b/tests/baselines/reference/propertyAccessWidening.types @@ -0,0 +1,100 @@ +=== tests/cases/conformance/expressions/propertyAccess/propertyAccessWidening.ts === +// Repro from #31762 + +function g1(headerNames: any) { +>g1 : (headerNames: any) => void +>headerNames : any + + let t = [{ hasLineBreak: false, cells: [] }]; +>t : { hasLineBreak: boolean; cells: never[]; }[] +>[{ hasLineBreak: false, cells: [] }] : { hasLineBreak: boolean; cells: never[]; }[] +>{ hasLineBreak: false, cells: [] } : { hasLineBreak: boolean; cells: never[]; } +>hasLineBreak : boolean +>false : false +>cells : never[] +>[] : never[] + + const table = [{cells: headerNames }].concat(t); +>table : { cells: any; }[] +>[{cells: headerNames }].concat(t) : { cells: any; }[] +>[{cells: headerNames }].concat : { (...items: ConcatArray<{ cells: any; }>[]): { cells: any; }[]; (...items: ({ cells: any; } | ConcatArray<{ cells: any; }>)[]): { cells: any; }[]; } +>[{cells: headerNames }] : { cells: any; }[] +>{cells: headerNames } : { cells: any; } +>cells : any +>headerNames : any +>concat : { (...items: ConcatArray<{ cells: any; }>[]): { cells: any; }[]; (...items: ({ cells: any; } | ConcatArray<{ cells: any; }>)[]): { cells: any; }[]; } +>t : { hasLineBreak: boolean; cells: never[]; }[] +} + +function g2(headerNames: any) { +>g2 : (headerNames: any) => void +>headerNames : any + + let t = [{ hasLineBreak: false, cells: [] }]; +>t : { hasLineBreak: boolean; cells: never[]; }[] +>[{ hasLineBreak: false, cells: [] }] : { hasLineBreak: boolean; cells: never[]; }[] +>{ hasLineBreak: false, cells: [] } : { hasLineBreak: boolean; cells: never[]; } +>hasLineBreak : boolean +>false : false +>cells : never[] +>[] : never[] + + const table = [{cells: headerNames }]["concat"](t); +>table : { cells: any; }[] +>[{cells: headerNames }]["concat"](t) : { cells: any; }[] +>[{cells: headerNames }]["concat"] : { (...items: ConcatArray<{ cells: any; }>[]): { cells: any; }[]; (...items: ({ cells: any; } | ConcatArray<{ cells: any; }>)[]): { cells: any; }[]; } +>[{cells: headerNames }] : { cells: any; }[] +>{cells: headerNames } : { cells: any; } +>cells : any +>headerNames : any +>"concat" : "concat" +>t : { hasLineBreak: boolean; cells: never[]; }[] +} + +// Object in property or element access is widened when target of assignment + +function foo(options?: { a: string, b: number }) { +>foo : (options?: { a: string; b: number; } | undefined) => void +>options : { a: string; b: number; } | undefined +>a : string +>b : number + + let x1 = (options || {}).a; // Object type not widened +>x1 : string | undefined +>(options || {}).a : string | undefined +>(options || {}) : { a: string; b: number; } | {} +>options || {} : { a: string; b: number; } | {} +>options : { a: string; b: number; } | undefined +>{} : {} +>a : string | undefined + + let x2 = (options || {})["a"]; // Object type not widened +>x2 : string | undefined +>(options || {})["a"] : string | undefined +>(options || {}) : { a: string; b: number; } | {} +>options || {} : { a: string; b: number; } | {} +>options : { a: string; b: number; } | undefined +>{} : {} +>"a" : "a" + + (options || {}).a = 1; // Object type widened, error +>(options || {}).a = 1 : 1 +>(options || {}).a : any +>(options || {}) : { a: string; b: number; } | {} +>options || {} : { a: string; b: number; } | {} +>options : { a: string; b: number; } | undefined +>{} : {} +>a : any +>1 : 1 + + (options || {})["a"] = 1; // Object type widened, error +>(options || {})["a"] = 1 : 1 +>(options || {})["a"] : any +>(options || {}) : { a: string; b: number; } | {} +>options || {} : { a: string; b: number; } | {} +>options : { a: string; b: number; } | undefined +>{} : {} +>"a" : "a" +>1 : 1 +} + diff --git a/tests/cases/conformance/expressions/propertyAccess/propertyAccessWidening.ts b/tests/cases/conformance/expressions/propertyAccess/propertyAccessWidening.ts new file mode 100644 index 0000000000000..ddcad3f57c97f --- /dev/null +++ b/tests/cases/conformance/expressions/propertyAccess/propertyAccessWidening.ts @@ -0,0 +1,22 @@ +// @strict: true + +// Repro from #31762 + +function g1(headerNames: any) { + let t = [{ hasLineBreak: false, cells: [] }]; + const table = [{cells: headerNames }].concat(t); +} + +function g2(headerNames: any) { + let t = [{ hasLineBreak: false, cells: [] }]; + const table = [{cells: headerNames }]["concat"](t); +} + +// Object in property or element access is widened when target of assignment + +function foo(options?: { a: string, b: number }) { + let x1 = (options || {}).a; // Object type not widened + let x2 = (options || {})["a"]; // Object type not widened + (options || {}).a = 1; // Object type widened, error + (options || {})["a"] = 1; // Object type widened, error +}