Skip to content

Commit 89f43d1

Browse files
authored
Merge pull request #14273 from Microsoft/infereClassPropertiesFromMethods
Infer class properties from methods and not just constructors in .js files
2 parents b3161e3 + 3705b87 commit 89f43d1

File tree

6 files changed

+785
-32
lines changed

6 files changed

+785
-32
lines changed

src/compiler/binder.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,23 +2297,28 @@ namespace ts {
22972297

22982298
function bindThisPropertyAssignment(node: BinaryExpression) {
22992299
Debug.assert(isInJavaScriptFile(node));
2300-
// Declare a 'member' if the container is an ES5 class or ES6 constructor
2301-
if (container.kind === SyntaxKind.FunctionDeclaration || container.kind === SyntaxKind.FunctionExpression) {
2302-
container.symbol.members = container.symbol.members || createMap<Symbol>();
2303-
// It's acceptable for multiple 'this' assignments of the same identifier to occur
2304-
declareSymbol(container.symbol.members, container.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property);
2305-
}
2306-
else if (container.kind === SyntaxKind.Constructor) {
2307-
// this.foo assignment in a JavaScript class
2308-
// Bind this property to the containing class
2309-
const saveContainer = container;
2310-
container = container.parent;
2311-
const symbol = bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property, SymbolFlags.None);
2312-
if (symbol) {
2313-
// constructor-declared symbols can be overwritten by subsequent method declarations
2314-
(symbol as Symbol).isReplaceableByMethod = true;
2315-
}
2316-
container = saveContainer;
2300+
switch (container.kind) {
2301+
case SyntaxKind.FunctionDeclaration:
2302+
case SyntaxKind.FunctionExpression:
2303+
// Declare a 'member' if the container is an ES5 class or ES6 constructor
2304+
container.symbol.members = container.symbol.members || createMap<Symbol>();
2305+
// It's acceptable for multiple 'this' assignments of the same identifier to occur
2306+
declareSymbol(container.symbol.members, container.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property);
2307+
break;
2308+
2309+
case SyntaxKind.Constructor:
2310+
case SyntaxKind.MethodDeclaration:
2311+
case SyntaxKind.GetAccessor:
2312+
case SyntaxKind.SetAccessor:
2313+
// this.foo assignment in a JavaScript class
2314+
// Bind this property to the containing class
2315+
const containingClass = container.parent;
2316+
const symbol = declareSymbol(hasModifier(container, ModifierFlags.Static) ? containingClass.symbol.exports : containingClass.symbol.members, containingClass.symbol, node, SymbolFlags.Property, SymbolFlags.None);
2317+
if (symbol) {
2318+
// symbols declared through 'this' property assignements can be overwritten by subsequent method declarations
2319+
(symbol as Symbol).isReplaceableByMethod = true;
2320+
}
2321+
break;
23172322
}
23182323
}
23192324

src/compiler/checker.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3488,25 +3488,41 @@ namespace ts {
34883488
return undefined;
34893489
}
34903490

3491-
// Return the inferred type for a variable, parameter, or property declaration
3492-
function getTypeForJSSpecialPropertyDeclaration(declaration: Declaration): Type {
3493-
const expression = declaration.kind === SyntaxKind.BinaryExpression ? <BinaryExpression>declaration :
3494-
declaration.kind === SyntaxKind.PropertyAccessExpression ? <BinaryExpression>getAncestor(declaration, SyntaxKind.BinaryExpression) :
3495-
undefined;
3491+
function getWidenedTypeFromJSSpecialPropertyDeclarations(symbol: Symbol) {
3492+
const types: Type[] = [];
3493+
let definedInConstructor = false;
3494+
let definedInMethod = false;
3495+
for (const declaration of symbol.declarations) {
3496+
const expression = declaration.kind === SyntaxKind.BinaryExpression ? <BinaryExpression>declaration :
3497+
declaration.kind === SyntaxKind.PropertyAccessExpression ? <BinaryExpression>getAncestor(declaration, SyntaxKind.BinaryExpression) :
3498+
undefined;
34963499

3497-
if (!expression) {
3498-
return unknownType;
3499-
}
3500+
if (!expression) {
3501+
return unknownType;
3502+
}
35003503

3501-
if (expression.flags & NodeFlags.JavaScriptFile) {
3502-
// If there is a JSDoc type, use it
3503-
const type = getTypeForDeclarationFromJSDocComment(expression.parent);
3504-
if (type && type !== unknownType) {
3505-
return getWidenedType(type);
3504+
if (isPropertyAccessExpression(expression.left) && expression.left.expression.kind === SyntaxKind.ThisKeyword) {
3505+
if (getThisContainer(expression, /*includeArrowFunctions*/ false).kind === SyntaxKind.Constructor) {
3506+
definedInConstructor = true;
3507+
}
3508+
else {
3509+
definedInMethod = true;
3510+
}
3511+
}
3512+
3513+
if (expression.flags & NodeFlags.JavaScriptFile) {
3514+
// If there is a JSDoc type, use it
3515+
const type = getTypeForDeclarationFromJSDocComment(expression.parent);
3516+
if (type && type !== unknownType) {
3517+
types.push(getWidenedType(type));
3518+
continue;
3519+
}
35063520
}
3521+
3522+
types.push(getWidenedLiteralType(checkExpressionCached(expression.right)));
35073523
}
35083524

3509-
return getWidenedLiteralType(checkExpressionCached(expression.right));
3525+
return getWidenedType(addOptionality(getUnionType(types, /*subtypeReduction*/ true), definedInMethod && !definedInConstructor));
35103526
}
35113527

35123528
// Return the type implied by a binding pattern element. This is the type of the initializer of the element if
@@ -3663,7 +3679,7 @@ namespace ts {
36633679
// * className.prototype.method = expr
36643680
if (declaration.kind === SyntaxKind.BinaryExpression ||
36653681
declaration.kind === SyntaxKind.PropertyAccessExpression && declaration.parent.kind === SyntaxKind.BinaryExpression) {
3666-
type = getWidenedType(getUnionType(map(symbol.declarations, getTypeForJSSpecialPropertyDeclaration), /*subtypeReduction*/ true));
3682+
type = getWidenedTypeFromJSSpecialPropertyDeclarations(symbol);
36673683
}
36683684
else {
36693685
type = getWidenedTypeForVariableLikeDeclaration(<VariableLikeDeclaration>declaration, /*reportErrors*/ true);
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//// [tests/cases/conformance/salsa/inferringClassMembersFromAssignments.ts] ////
2+
3+
//// [a.js]
4+
5+
class C {
6+
constructor() {
7+
if (Math.random()) {
8+
this.inConstructor = 0;
9+
}
10+
else {
11+
this.inConstructor = "string"
12+
}
13+
this.inMultiple = 0;
14+
}
15+
method() {
16+
if (Math.random()) {
17+
this.inMethod = 0;
18+
}
19+
else {
20+
this.inMethod = "string"
21+
}
22+
this.inMultiple = "string";
23+
}
24+
get() {
25+
if (Math.random()) {
26+
this.inGetter = 0;
27+
}
28+
else {
29+
this.inGetter = "string"
30+
}
31+
this.inMultiple = false;
32+
}
33+
set() {
34+
if (Math.random()) {
35+
this.inSetter = 0;
36+
}
37+
else {
38+
this.inSetter = "string"
39+
}
40+
}
41+
static method() {
42+
if (Math.random()) {
43+
this.inStaticMethod = 0;
44+
}
45+
else {
46+
this.inStaticMethod = "string"
47+
}
48+
}
49+
static get() {
50+
if (Math.random()) {
51+
this.inStaticGetter = 0;
52+
}
53+
else {
54+
this.inStaticGetter = "string"
55+
}
56+
}
57+
static set() {
58+
if (Math.random()) {
59+
this.inStaticSetter = 0;
60+
}
61+
else {
62+
this.inStaticSetter = "string"
63+
}
64+
}
65+
}
66+
67+
//// [b.ts]
68+
var c = new C();
69+
70+
var stringOrNumber: string | number;
71+
var stringOrNumber = c.inConstructor;
72+
73+
var stringOrNumberOrUndefined: string | number | undefined;
74+
75+
var stringOrNumberOrUndefined = c.inMethod;
76+
var stringOrNumberOrUndefined = c.inGetter;
77+
var stringOrNumberOrUndefined = c.inSetter;
78+
79+
var stringOrNumberOrBoolean: string | number | boolean;
80+
81+
var stringOrNumberOrBoolean = c.inMultiple;
82+
83+
84+
var stringOrNumberOrUndefined = C.inStaticMethod;
85+
var stringOrNumberOrUndefined = C.inStaticGetter;
86+
var stringOrNumberOrUndefined = C.inStaticSetter;
87+
88+
89+
//// [output.js]
90+
var C = (function () {
91+
function C() {
92+
if (Math.random()) {
93+
this.inConstructor = 0;
94+
}
95+
else {
96+
this.inConstructor = "string";
97+
}
98+
this.inMultiple = 0;
99+
}
100+
C.prototype.method = function () {
101+
if (Math.random()) {
102+
this.inMethod = 0;
103+
}
104+
else {
105+
this.inMethod = "string";
106+
}
107+
this.inMultiple = "string";
108+
};
109+
C.prototype.get = function () {
110+
if (Math.random()) {
111+
this.inGetter = 0;
112+
}
113+
else {
114+
this.inGetter = "string";
115+
}
116+
this.inMultiple = false;
117+
};
118+
C.prototype.set = function () {
119+
if (Math.random()) {
120+
this.inSetter = 0;
121+
}
122+
else {
123+
this.inSetter = "string";
124+
}
125+
};
126+
C.method = function () {
127+
if (Math.random()) {
128+
this.inStaticMethod = 0;
129+
}
130+
else {
131+
this.inStaticMethod = "string";
132+
}
133+
};
134+
C.get = function () {
135+
if (Math.random()) {
136+
this.inStaticGetter = 0;
137+
}
138+
else {
139+
this.inStaticGetter = "string";
140+
}
141+
};
142+
C.set = function () {
143+
if (Math.random()) {
144+
this.inStaticSetter = 0;
145+
}
146+
else {
147+
this.inStaticSetter = "string";
148+
}
149+
};
150+
return C;
151+
}());
152+
var c = new C();
153+
var stringOrNumber;
154+
var stringOrNumber = c.inConstructor;
155+
var stringOrNumberOrUndefined;
156+
var stringOrNumberOrUndefined = c.inMethod;
157+
var stringOrNumberOrUndefined = c.inGetter;
158+
var stringOrNumberOrUndefined = c.inSetter;
159+
var stringOrNumberOrBoolean;
160+
var stringOrNumberOrBoolean = c.inMultiple;
161+
var stringOrNumberOrUndefined = C.inStaticMethod;
162+
var stringOrNumberOrUndefined = C.inStaticGetter;
163+
var stringOrNumberOrUndefined = C.inStaticSetter;

0 commit comments

Comments
 (0)