Skip to content

Commit c9ac15a

Browse files
authored
In JS, this assignments in constructors are preferred and nullable initializers become any (#22882)
* First draft:in js, constructor declaration is preferred * Add tests * initializer of null|undefined gives any in JS Also move this-assignment fixes out of binder. I'm going to put it in the checker instead. * In JS, initializer null|undefined: any, []: any[] * First draft of js prefer-ctor-types overhaul * Update tests, update baselines * Improve readability of constructor-type preference * Cleanup: Remove TODO and duplication * Add noImplicitAny errors * Add comment
1 parent fa794f6 commit c9ac15a

18 files changed

+1340
-121
lines changed

src/compiler/checker.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4255,10 +4255,12 @@ namespace ts {
42554255
return getWidenedLiteralType(checkExpressionCached(specialDeclaration));
42564256
}
42574257
const types: Type[] = [];
4258+
let constructorTypes: Type[];
42584259
let definedInConstructor = false;
42594260
let definedInMethod = false;
42604261
let jsDocType: Type;
42614262
for (const declaration of symbol.declarations) {
4263+
let declarationInConstructor = false;
42624264
const expression = declaration.kind === SyntaxKind.BinaryExpression ? <BinaryExpression>declaration :
42634265
declaration.kind === SyntaxKind.PropertyAccessExpression ? <BinaryExpression>getAncestor(declaration, SyntaxKind.BinaryExpression) :
42644266
undefined;
@@ -4271,9 +4273,10 @@ namespace ts {
42714273
const thisContainer = getThisContainer(expression, /*includeArrowFunctions*/ false);
42724274
// Properties defined in a constructor (or javascript constructor function) don't get undefined added.
42734275
// Function expressions that are assigned to the prototype count as methods.
4274-
if (thisContainer.kind === SyntaxKind.Constructor ||
4276+
declarationInConstructor = thisContainer.kind === SyntaxKind.Constructor ||
42754277
thisContainer.kind === SyntaxKind.FunctionDeclaration ||
4276-
(thisContainer.kind === SyntaxKind.FunctionExpression && !isPrototypePropertyAssignment(thisContainer.parent))) {
4278+
(thisContainer.kind === SyntaxKind.FunctionExpression && !isPrototypePropertyAssignment(thisContainer.parent));
4279+
if (declarationInConstructor) {
42774280
definedInConstructor = true;
42784281
}
42794282
else {
@@ -4296,14 +4299,37 @@ namespace ts {
42964299
}
42974300
else if (!jsDocType) {
42984301
// If we don't have an explicit JSDoc type, get the type from the expression.
4299-
types.push(getWidenedLiteralType(checkExpressionCached(expression.right)));
4302+
const type = getWidenedLiteralType(checkExpressionCached(expression.right));
4303+
let anyedType = type;
4304+
if (isEmptyArrayLiteralType(type)) {
4305+
anyedType = anyArrayType;
4306+
if (noImplicitAny) {
4307+
reportImplicitAnyError(expression, anyArrayType);
4308+
}
4309+
}
4310+
types.push(anyedType);
4311+
if (declarationInConstructor) {
4312+
(constructorTypes || (constructorTypes = [])).push(anyedType);
4313+
}
43004314
}
43014315
}
4302-
4303-
const type = jsDocType || getUnionType(types, UnionReduction.Subtype);
4304-
return getWidenedType(addOptionality(type, definedInMethod && !definedInConstructor));
4316+
let type = jsDocType;
4317+
if (!type) {
4318+
// use only the constructor types unless only null | undefined (including widening variants) were assigned there
4319+
const sourceTypes = some(constructorTypes, t => !!(t.flags & ~(TypeFlags.Nullable | TypeFlags.ContainsWideningType))) ? constructorTypes : types;
4320+
type = getUnionType(sourceTypes, UnionReduction.Subtype);
4321+
}
4322+
const widened = getWidenedType(addOptionality(type, definedInMethod && !definedInConstructor));
4323+
if (filterType(widened, t => !!(t.flags & ~TypeFlags.Nullable)) === neverType) {
4324+
if (noImplicitAny) {
4325+
reportImplicitAnyError(symbol.valueDeclaration, anyType);
4326+
}
4327+
return anyType;
4328+
}
4329+
return widened;
43054330
}
43064331

4332+
43074333
// Return the type implied by a binding pattern element. This is the type of the initializer of the element if
43084334
// one is present. Otherwise, if the element is itself a binding pattern, it is the type implied by the binding
43094335
// pattern. Otherwise, it is the type any.
@@ -11394,6 +11420,7 @@ namespace ts {
1139411420
const typeAsString = typeToString(getWidenedType(type));
1139511421
let diagnostic: DiagnosticMessage;
1139611422
switch (declaration.kind) {
11423+
case SyntaxKind.BinaryExpression:
1139711424
case SyntaxKind.PropertyDeclaration:
1139811425
case SyntaxKind.PropertySignature:
1139911426
diagnostic = Diagnostics.Member_0_implicitly_has_an_1_type;
@@ -19689,11 +19716,27 @@ namespace ts {
1968919716
}
1969019717

1969119718
function checkDeclarationInitializer(declaration: HasExpressionInitializer) {
19692-
const initializer = isInJavaScriptFile(declaration) && getDeclaredJavascriptInitializer(declaration) || declaration.initializer;
19719+
const inJs = isInJavaScriptFile(declaration);
19720+
const initializer = inJs && getDeclaredJavascriptInitializer(declaration) || declaration.initializer;
1969319721
const type = getTypeOfExpression(initializer, /*cache*/ true);
19694-
return getCombinedNodeFlags(declaration) & NodeFlags.Const ||
19722+
const widened = getCombinedNodeFlags(declaration) & NodeFlags.Const ||
1969519723
(getCombinedModifierFlags(declaration) & ModifierFlags.Readonly && !isParameterPropertyDeclaration(declaration)) ||
1969619724
isTypeAssertion(initializer) ? type : getWidenedLiteralType(type);
19725+
if (inJs) {
19726+
if (widened.flags & TypeFlags.Nullable) {
19727+
if (noImplicitAny) {
19728+
reportImplicitAnyError(declaration, anyType);
19729+
}
19730+
return anyType;
19731+
}
19732+
else if (isEmptyArrayLiteralType(widened)) {
19733+
if (noImplicitAny) {
19734+
reportImplicitAnyError(declaration, anyArrayType);
19735+
}
19736+
return anyArrayType;
19737+
}
19738+
}
19739+
return widened;
1969719740
}
1969819741

1969919742
function isLiteralOfContextualType(candidateType: Type, contextualType: Type): boolean {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
tests/cases/conformance/salsa/a.js(14,13): error TS7008: Member 'inMethodNullable' implicitly has an 'any' type.
2+
tests/cases/conformance/salsa/a.js(20,9): error TS2322: Type '"string"' is not assignable to type 'number'.
3+
tests/cases/conformance/salsa/a.js(39,9): error TS2322: Type 'false' is not assignable to type 'number'.
4+
tests/cases/conformance/salsa/a.js(93,13): error TS2334: 'this' cannot be referenced in a static property initializer.
5+
tests/cases/conformance/salsa/a.js(96,13): error TS2334: 'this' cannot be referenced in a static property initializer.
6+
7+
8+
==== tests/cases/conformance/salsa/a.js (5 errors) ====
9+
class C {
10+
constructor() {
11+
if (Math.random()) {
12+
this.inConstructor = 0;
13+
}
14+
else {
15+
this.inConstructor = "string"
16+
}
17+
this.inMultiple = 0;
18+
}
19+
method() {
20+
if (Math.random()) {
21+
this.inMethod = 0;
22+
this.inMethodNullable = null;
23+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24+
!!! error TS7008: Member 'inMethodNullable' implicitly has an 'any' type.
25+
}
26+
else {
27+
this.inMethod = "string"
28+
this.inMethodNullable = undefined;
29+
}
30+
this.inMultiple = "string";
31+
~~~~~~~~~~~~~~~
32+
!!! error TS2322: Type '"string"' is not assignable to type 'number'.
33+
this.inMultipleMethods = "string";
34+
35+
var action = () => {
36+
if (Math.random()) {
37+
this.inNestedArrowFunction = 0;
38+
}
39+
else {
40+
this.inNestedArrowFunction = "string"
41+
}
42+
};
43+
}
44+
get() {
45+
if (Math.random()) {
46+
this.inGetter = 0;
47+
}
48+
else {
49+
this.inGetter = "string"
50+
}
51+
this.inMultiple = false;
52+
~~~~~~~~~~~~~~~
53+
!!! error TS2322: Type 'false' is not assignable to type 'number'.
54+
this.inMultipleMethods = false;
55+
}
56+
set() {
57+
if (Math.random()) {
58+
this.inSetter = 0;
59+
}
60+
else {
61+
this.inSetter = "string"
62+
}
63+
}
64+
prop = () => {
65+
if (Math.random()) {
66+
this.inPropertyDeclaration = 0;
67+
}
68+
else {
69+
this.inPropertyDeclaration = "string"
70+
}
71+
}
72+
static method() {
73+
if (Math.random()) {
74+
this.inStaticMethod = 0;
75+
}
76+
else {
77+
this.inStaticMethod = "string"
78+
}
79+
80+
var action = () => {
81+
if (Math.random()) {
82+
this.inStaticNestedArrowFunction = 0;
83+
}
84+
else {
85+
this.inStaticNestedArrowFunction = "string"
86+
}
87+
};
88+
}
89+
static get() {
90+
if (Math.random()) {
91+
this.inStaticGetter = 0;
92+
}
93+
else {
94+
this.inStaticGetter = "string"
95+
}
96+
}
97+
static set() {
98+
if (Math.random()) {
99+
this.inStaticSetter = 0;
100+
}
101+
else {
102+
this.inStaticSetter = "string"
103+
}
104+
}
105+
static prop = () => {
106+
if (Math.random()) {
107+
this.inStaticPropertyDeclaration = 0;
108+
~~~~
109+
!!! error TS2334: 'this' cannot be referenced in a static property initializer.
110+
}
111+
else {
112+
this.inStaticPropertyDeclaration = "string"
113+
~~~~
114+
!!! error TS2334: 'this' cannot be referenced in a static property initializer.
115+
}
116+
}
117+
}
118+
119+
==== tests/cases/conformance/salsa/b.ts (0 errors) ====
120+
var c = new C();
121+
122+
var stringOrNumber: string | number;
123+
var stringOrNumber = c.inConstructor;
124+
125+
var stringOrNumberOrUndefined: string | number | undefined;
126+
127+
var stringOrNumberOrUndefined = c.inMethod;
128+
var stringOrNumberOrUndefined = c.inGetter;
129+
var stringOrNumberOrUndefined = c.inSetter;
130+
var stringOrNumberOrUndefined = c.inPropertyDeclaration;
131+
var stringOrNumberOrUndefined = c.inNestedArrowFunction
132+
133+
var stringOrNumberOrBoolean: string | number | boolean;
134+
135+
var number: number;
136+
var number = c.inMultiple;
137+
var stringOrBooleanOrUndefined : string | boolean | undefined;
138+
var stringOrBooleanOrUndefined = c.inMultipleMethods;
139+
var any: any;
140+
var any = c.inMethodNullable;
141+
142+
143+
var stringOrNumberOrUndefined = C.inStaticMethod;
144+
var stringOrNumberOrUndefined = C.inStaticGetter;
145+
var stringOrNumberOrUndefined = C.inStaticSetter;
146+
var stringOrNumberOrUndefined = C.inStaticPropertyDeclaration;
147+
var stringOrNumberOrUndefined = C.inStaticNestedArrowFunction;
148+

tests/baselines/reference/inferringClassMembersFromAssignments.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ class C {
1414
method() {
1515
if (Math.random()) {
1616
this.inMethod = 0;
17+
this.inMethodNullable = null;
1718
}
1819
else {
1920
this.inMethod = "string"
21+
this.inMethodNullable = undefined;
2022
}
2123
this.inMultiple = "string";
24+
this.inMultipleMethods = "string";
2225

2326
var action = () => {
2427
if (Math.random()) {
@@ -37,6 +40,7 @@ class C {
3740
this.inGetter = "string"
3841
}
3942
this.inMultiple = false;
43+
this.inMultipleMethods = false;
4044
}
4145
set() {
4246
if (Math.random()) {
@@ -113,7 +117,12 @@ var stringOrNumberOrUndefined = c.inNestedArrowFunction
113117

114118
var stringOrNumberOrBoolean: string | number | boolean;
115119

116-
var stringOrNumberOrBoolean = c.inMultiple;
120+
var number: number;
121+
var number = c.inMultiple;
122+
var stringOrBooleanOrUndefined : string | boolean | undefined;
123+
var stringOrBooleanOrUndefined = c.inMultipleMethods;
124+
var any: any;
125+
var any = c.inMethodNullable;
117126

118127

119128
var stringOrNumberOrUndefined = C.inStaticMethod;
@@ -148,11 +157,14 @@ var C = /** @class */ (function () {
148157
var _this = this;
149158
if (Math.random()) {
150159
this.inMethod = 0;
160+
this.inMethodNullable = null;
151161
}
152162
else {
153163
this.inMethod = "string";
164+
this.inMethodNullable = undefined;
154165
}
155166
this.inMultiple = "string";
167+
this.inMultipleMethods = "string";
156168
var action = function () {
157169
if (Math.random()) {
158170
_this.inNestedArrowFunction = 0;
@@ -170,6 +182,7 @@ var C = /** @class */ (function () {
170182
this.inGetter = "string";
171183
}
172184
this.inMultiple = false;
185+
this.inMultipleMethods = false;
173186
};
174187
C.prototype.set = function () {
175188
if (Math.random()) {
@@ -232,7 +245,12 @@ var stringOrNumberOrUndefined = c.inSetter;
232245
var stringOrNumberOrUndefined = c.inPropertyDeclaration;
233246
var stringOrNumberOrUndefined = c.inNestedArrowFunction;
234247
var stringOrNumberOrBoolean;
235-
var stringOrNumberOrBoolean = c.inMultiple;
248+
var number;
249+
var number = c.inMultiple;
250+
var stringOrBooleanOrUndefined;
251+
var stringOrBooleanOrUndefined = c.inMultipleMethods;
252+
var any;
253+
var any = c.inMethodNullable;
236254
var stringOrNumberOrUndefined = C.inStaticMethod;
237255
var stringOrNumberOrUndefined = C.inStaticGetter;
238256
var stringOrNumberOrUndefined = C.inStaticSetter;

0 commit comments

Comments
 (0)