Skip to content

Commit 02ccd91

Browse files
committed
Infer class properties from methods and not just constructors
1 parent 65da012 commit 02ccd91

6 files changed

+717
-32
lines changed

src/compiler/binder.ts

+25-17
Original file line numberDiff line numberDiff line change
@@ -2238,23 +2238,31 @@ namespace ts {
22382238

22392239
function bindThisPropertyAssignment(node: BinaryExpression) {
22402240
Debug.assert(isInJavaScriptFile(node));
2241-
// Declare a 'member' if the container is an ES5 class or ES6 constructor
2242-
if (container.kind === SyntaxKind.FunctionDeclaration || container.kind === SyntaxKind.FunctionExpression) {
2243-
container.symbol.members = container.symbol.members || createMap<Symbol>();
2244-
// It's acceptable for multiple 'this' assignments of the same identifier to occur
2245-
declareSymbol(container.symbol.members, container.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property);
2246-
}
2247-
else if (container.kind === SyntaxKind.Constructor) {
2248-
// this.foo assignment in a JavaScript class
2249-
// Bind this property to the containing class
2250-
const saveContainer = container;
2251-
container = container.parent;
2252-
const symbol = bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property, SymbolFlags.None);
2253-
if (symbol) {
2254-
// constructor-declared symbols can be overwritten by subsequent method declarations
2255-
(symbol as Symbol).isReplaceableByMethod = true;
2256-
}
2257-
container = saveContainer;
2241+
switch (container.kind) {
2242+
case SyntaxKind.FunctionDeclaration:
2243+
case SyntaxKind.FunctionExpression:
2244+
// Declare a 'member' if the container is an ES5 class or ES6 constructor
2245+
container.symbol.members = container.symbol.members || createMap<Symbol>();
2246+
// It's acceptable for multiple 'this' assignments of the same identifier to occur
2247+
declareSymbol(container.symbol.members, container.symbol, node, SymbolFlags.Property, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property);
2248+
break;
2249+
2250+
case SyntaxKind.Constructor:
2251+
case SyntaxKind.MethodDeclaration:
2252+
case SyntaxKind.GetAccessor:
2253+
case SyntaxKind.SetAccessor:
2254+
// this.foo assignment in a JavaScript class
2255+
// Bind this property to the containing class
2256+
const containingClass = container.parent;
2257+
const symbol = hasModifier(container, ModifierFlags.Static)
2258+
? declareSymbol(containingClass.symbol.exports, containingClass.symbol, node, SymbolFlags.Property, SymbolFlags.None)
2259+
: declareSymbol(containingClass.symbol.members, containingClass.symbol, node, SymbolFlags.Property, SymbolFlags.None);
2260+
2261+
if (symbol) {
2262+
// symbols declared through 'this' property assignements can be overwritten by subsequent method declarations
2263+
(symbol as Symbol).isReplaceableByMethod = true;
2264+
}
2265+
break;
22582266
}
22592267
}
22602268

src/compiler/checker.ts

+31-15
Original file line numberDiff line numberDiff line change
@@ -3472,25 +3472,41 @@ namespace ts {
34723472
return undefined;
34733473
}
34743474

3475-
// Return the inferred type for a variable, parameter, or property declaration
3476-
function getTypeForJSSpecialPropertyDeclaration(declaration: Declaration): Type {
3477-
const expression = declaration.kind === SyntaxKind.BinaryExpression ? <BinaryExpression>declaration :
3478-
declaration.kind === SyntaxKind.PropertyAccessExpression ? <BinaryExpression>getAncestor(declaration, SyntaxKind.BinaryExpression) :
3479-
undefined;
3475+
function getWidendedTypeFromJSSpecialPropetyDeclarations(symbol: Symbol) {
3476+
const types: Type[] = [];
3477+
let definedInConstructor = false;
3478+
let definedInMethod = false;
3479+
for (const declaration of symbol.declarations) {
3480+
const expression = declaration.kind === SyntaxKind.BinaryExpression ? <BinaryExpression>declaration :
3481+
declaration.kind === SyntaxKind.PropertyAccessExpression ? <BinaryExpression>getAncestor(declaration, SyntaxKind.BinaryExpression) :
3482+
undefined;
34803483

3481-
if (!expression) {
3482-
return unknownType;
3483-
}
3484+
if (!expression) {
3485+
return unknownType;
3486+
}
34843487

3485-
if (expression.flags & NodeFlags.JavaScriptFile) {
3486-
// If there is a JSDoc type, use it
3487-
const type = getTypeForDeclarationFromJSDocComment(expression.parent);
3488-
if (type && type !== unknownType) {
3489-
return getWidenedType(type);
3488+
if (isPropertyAccessExpression(expression.left) && expression.left.expression.kind === SyntaxKind.ThisKeyword) {
3489+
if (getThisContainer(expression, /*includeArrowFunctions*/ false).kind === SyntaxKind.Constructor) {
3490+
definedInConstructor = true;
3491+
}
3492+
else {
3493+
definedInMethod = true;
3494+
}
3495+
}
3496+
3497+
if (expression.flags & NodeFlags.JavaScriptFile) {
3498+
// If there is a JSDoc type, use it
3499+
const type = getTypeForDeclarationFromJSDocComment(expression.parent);
3500+
if (type && type !== unknownType) {
3501+
types.push(getWidenedType(type));
3502+
continue;
3503+
}
34903504
}
3505+
3506+
types.push(getWidenedLiteralType(checkExpressionCached(expression.right)));
34913507
}
34923508

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

34963512
// Return the type implied by a binding pattern element. This is the type of the initializer of the element if
@@ -3647,7 +3663,7 @@ namespace ts {
36473663
// * className.prototype.method = expr
36483664
if (declaration.kind === SyntaxKind.BinaryExpression ||
36493665
declaration.kind === SyntaxKind.PropertyAccessExpression && declaration.parent.kind === SyntaxKind.BinaryExpression) {
3650-
type = getWidenedType(getUnionType(map(symbol.declarations, getTypeForJSSpecialPropertyDeclaration), /*subtypeReduction*/ true));
3666+
type = getWidendedTypeFromJSSpecialPropetyDeclarations(symbol);
36513667
}
36523668
else {
36533669
type = getWidenedTypeForVariableLikeDeclaration(<VariableLikeDeclaration>declaration, /*reportErrors*/ true);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
}
14+
method() {
15+
if (Math.random()) {
16+
this.inMethod = 0;
17+
}
18+
else {
19+
this.inMethod = "string"
20+
}
21+
}
22+
get() {
23+
if (Math.random()) {
24+
this.inGetter = 0;
25+
}
26+
else {
27+
this.inGetter = "string"
28+
}
29+
}
30+
set() {
31+
if (Math.random()) {
32+
this.inSetter = 0;
33+
}
34+
else {
35+
this.inSetter = "string"
36+
}
37+
}
38+
static method() {
39+
if (Math.random()) {
40+
this.inStaticMethod = 0;
41+
}
42+
else {
43+
this.inStaticMethod = "string"
44+
}
45+
}
46+
static get() {
47+
if (Math.random()) {
48+
this.inStaticGetter = 0;
49+
}
50+
else {
51+
this.inStaticGetter = "string"
52+
}
53+
}
54+
static set() {
55+
if (Math.random()) {
56+
this.inStaticSetter = 0;
57+
}
58+
else {
59+
this.inStaticSetter = "string"
60+
}
61+
}
62+
}
63+
64+
//// [b.ts]
65+
var c = new C();
66+
67+
var stringOrNumber: string | number;
68+
var stringOrNumber = c.inConstructor;
69+
70+
var stringOrNumberOrUndefined: string | number | undefined;
71+
72+
var stringOrNumberOrUndefined = c.inMethod;
73+
var stringOrNumberOrUndefined = c.inGetter;
74+
var stringOrNumberOrUndefined = c.inSetter;
75+
76+
var stringOrNumberOrUndefined = C.inStaticMethod;
77+
var stringOrNumberOrUndefined = C.inStaticGetter;
78+
var stringOrNumberOrUndefined = C.inStaticSetter;
79+
80+
81+
//// [output.js]
82+
var C = (function () {
83+
function C() {
84+
if (Math.random()) {
85+
this.inConstructor = 0;
86+
}
87+
else {
88+
this.inConstructor = "string";
89+
}
90+
}
91+
C.prototype.method = function () {
92+
if (Math.random()) {
93+
this.inMethod = 0;
94+
}
95+
else {
96+
this.inMethod = "string";
97+
}
98+
};
99+
C.prototype.get = function () {
100+
if (Math.random()) {
101+
this.inGetter = 0;
102+
}
103+
else {
104+
this.inGetter = "string";
105+
}
106+
};
107+
C.prototype.set = function () {
108+
if (Math.random()) {
109+
this.inSetter = 0;
110+
}
111+
else {
112+
this.inSetter = "string";
113+
}
114+
};
115+
C.method = function () {
116+
if (Math.random()) {
117+
this.inStaticMethod = 0;
118+
}
119+
else {
120+
this.inStaticMethod = "string";
121+
}
122+
};
123+
C.get = function () {
124+
if (Math.random()) {
125+
this.inStaticGetter = 0;
126+
}
127+
else {
128+
this.inStaticGetter = "string";
129+
}
130+
};
131+
C.set = function () {
132+
if (Math.random()) {
133+
this.inStaticSetter = 0;
134+
}
135+
else {
136+
this.inStaticSetter = "string";
137+
}
138+
};
139+
return C;
140+
}());
141+
var c = new C();
142+
var stringOrNumber;
143+
var stringOrNumber = c.inConstructor;
144+
var stringOrNumberOrUndefined;
145+
var stringOrNumberOrUndefined = c.inMethod;
146+
var stringOrNumberOrUndefined = c.inGetter;
147+
var stringOrNumberOrUndefined = c.inSetter;
148+
var stringOrNumberOrUndefined = C.inStaticMethod;
149+
var stringOrNumberOrUndefined = C.inStaticGetter;
150+
var stringOrNumberOrUndefined = C.inStaticSetter;

0 commit comments

Comments
 (0)