Skip to content

Consistent widening in access expressions #31802

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20219,19 +20219,25 @@ namespace ts {
return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably? also skip both kinds of casts.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, if there is an intervening type assertion then that specifies the type and widening isn't relevant.

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);
Expand Down Expand Up @@ -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) {
Expand Down
34 changes: 34 additions & 0 deletions tests/baselines/reference/propertyAccessWidening.errors.txt
Original file line number Diff line number Diff line change
@@ -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 '{}'.
}

41 changes: 41 additions & 0 deletions tests/baselines/reference/propertyAccessWidening.js
Original file line number Diff line number Diff line change
@@ -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
}
65 changes: 65 additions & 0 deletions tests/baselines/reference/propertyAccessWidening.symbols
Original file line number Diff line number Diff line change
@@ -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))
}

100 changes: 100 additions & 0 deletions tests/baselines/reference/propertyAccessWidening.types
Original file line number Diff line number Diff line change
@@ -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
}

Original file line number Diff line number Diff line change
@@ -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
}