diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 00ddb416e2263..739c82287254d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4409,7 +4409,7 @@ namespace ts { const special = getSpecialPropertyAssignmentKind(expression); if (special === SpecialPropertyAssignmentKind.ThisProperty) { const thisContainer = getThisContainer(expression, /*includeArrowFunctions*/ false); - // Properties defined in a constructor (or javascript constructor function) don't get undefined added. + // Properties defined in a constructor (or base constructor, or javascript constructor function) don't get undefined added. // Function expressions that are assigned to the prototype count as methods. declarationInConstructor = thisContainer.kind === SyntaxKind.Constructor || thisContainer.kind === SyntaxKind.FunctionDeclaration || @@ -4479,7 +4479,14 @@ namespace ts { } let type = jsDocType; if (!type) { - // use only the constructor types unless only null | undefined (including widening variants) were assigned there + // use only the constructor types unless they were only assigned null | undefined (including widening variants) + if (definedInMethod) { + const propType = getTypeOfSpecialPropertyOfBaseType(symbol); + if (propType) { + (constructorTypes || (constructorTypes = [])).push(propType); + definedInConstructor = true; + } + } const sourceTypes = some(constructorTypes, t => !!(t.flags & ~(TypeFlags.Nullable | TypeFlags.ContainsWideningType))) ? constructorTypes : types; type = getUnionType(sourceTypes, UnionReduction.Subtype); } @@ -4493,6 +4500,20 @@ namespace ts { return widened; } + /** check for definition in base class if any declaration is in a class */ + function getTypeOfSpecialPropertyOfBaseType(specialProperty: Symbol) { + const parentDeclaration = forEach(specialProperty.declarations, d => { + const parent = getThisContainer(d, /*includeArrowFunctions*/ false).parent; + return isClassLike(parent) && parent; + }); + if (parentDeclaration) { + const classType = getDeclaredTypeOfSymbol(getSymbolOfNode(parentDeclaration)) as InterfaceType; + const baseClassType = classType && getBaseTypes(classType)[0]; + if (baseClassType) { + return getTypeOfPropertyOfType(baseClassType, specialProperty.escapedName); + } + } + } // Return the type implied by a binding pattern element. This is the type of the initializer of the element if // one is present. Otherwise, if the element is itself a binding pattern, it is the type implied by the binding diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments3.symbols b/tests/baselines/reference/inferringClassMembersFromAssignments3.symbols new file mode 100644 index 0000000000000..b4006600dbe58 --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments3.symbols @@ -0,0 +1,25 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + constructor() { + this.p = 1 +>this.p : Symbol(Base.p, Decl(a.js, 1, 19)) +>this : Symbol(Base, Decl(a.js, 0, 0)) +>p : Symbol(Base.p, Decl(a.js, 1, 19)) + } +} +class Derived extends Base { +>Derived : Symbol(Derived, Decl(a.js, 4, 1)) +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + m() { +>m : Symbol(Derived.m, Decl(a.js, 5, 28)) + + this.p = 1 +>this.p : Symbol(Derived.p, Decl(a.js, 6, 9)) +>this : Symbol(Derived, Decl(a.js, 4, 1)) +>p : Symbol(Derived.p, Decl(a.js, 6, 9)) + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments3.types b/tests/baselines/reference/inferringClassMembersFromAssignments3.types new file mode 100644 index 0000000000000..60d545b2832fe --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments3.types @@ -0,0 +1,29 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Base + + constructor() { + this.p = 1 +>this.p = 1 : 1 +>this.p : number +>this : this +>p : number +>1 : 1 + } +} +class Derived extends Base { +>Derived : Derived +>Base : Base + + m() { +>m : () => void + + this.p = 1 +>this.p = 1 : 1 +>this.p : number +>this : this +>p : number +>1 : 1 + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments4.symbols b/tests/baselines/reference/inferringClassMembersFromAssignments4.symbols new file mode 100644 index 0000000000000..d0d438c3ed381 --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments4.symbols @@ -0,0 +1,28 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + m() { +>m : Symbol(Base.m, Decl(a.js, 0, 12)) + + this.p = 1 +>this.p : Symbol(Base.p, Decl(a.js, 1, 9)) +>this : Symbol(Base, Decl(a.js, 0, 0)) +>p : Symbol(Base.p, Decl(a.js, 1, 9)) + } +} +class Derived extends Base { +>Derived : Symbol(Derived, Decl(a.js, 4, 1)) +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + m() { +>m : Symbol(Derived.m, Decl(a.js, 5, 28)) + + // should be OK, and p should have type number | undefined from its base + this.p = 1 +>this.p : Symbol(Derived.p, Decl(a.js, 6, 9)) +>this : Symbol(Derived, Decl(a.js, 4, 1)) +>p : Symbol(Derived.p, Decl(a.js, 6, 9)) + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments4.types b/tests/baselines/reference/inferringClassMembersFromAssignments4.types new file mode 100644 index 0000000000000..f20602d188e7d --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments4.types @@ -0,0 +1,32 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Base + + m() { +>m : () => void + + this.p = 1 +>this.p = 1 : 1 +>this.p : number | undefined +>this : this +>p : number | undefined +>1 : 1 + } +} +class Derived extends Base { +>Derived : Derived +>Base : Base + + m() { +>m : () => void + + // should be OK, and p should have type number | undefined from its base + this.p = 1 +>this.p = 1 : 1 +>this.p : number | undefined +>this : this +>p : number | undefined +>1 : 1 + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments5.symbols b/tests/baselines/reference/inferringClassMembersFromAssignments5.symbols new file mode 100644 index 0000000000000..5c46300d77009 --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments5.symbols @@ -0,0 +1,37 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + m() { +>m : Symbol(Base.m, Decl(a.js, 0, 12)) + + this.p = 1 +>this.p : Symbol(Base.p, Decl(a.js, 1, 9)) +>this : Symbol(Base, Decl(a.js, 0, 0)) +>p : Symbol(Base.p, Decl(a.js, 1, 9)) + } +} +class Derived extends Base { +>Derived : Symbol(Derived, Decl(a.js, 4, 1)) +>Base : Symbol(Base, Decl(a.js, 0, 0)) + + constructor() { + super(); +>super : Symbol(Base, Decl(a.js, 0, 0)) + + // should be OK, and p should have type number from this assignment + this.p = 1 +>this.p : Symbol(Derived.p, Decl(a.js, 7, 16)) +>this : Symbol(Derived, Decl(a.js, 4, 1)) +>p : Symbol(Derived.p, Decl(a.js, 7, 16)) + } + test() { +>test : Symbol(Derived.test, Decl(a.js, 10, 5)) + + return this.p +>this.p : Symbol(Derived.p, Decl(a.js, 7, 16)) +>this : Symbol(Derived, Decl(a.js, 4, 1)) +>p : Symbol(Derived.p, Decl(a.js, 7, 16)) + } +} + diff --git a/tests/baselines/reference/inferringClassMembersFromAssignments5.types b/tests/baselines/reference/inferringClassMembersFromAssignments5.types new file mode 100644 index 0000000000000..fc95b82e5fdeb --- /dev/null +++ b/tests/baselines/reference/inferringClassMembersFromAssignments5.types @@ -0,0 +1,42 @@ +=== tests/cases/conformance/salsa/a.js === +class Base { +>Base : Base + + m() { +>m : () => void + + this.p = 1 +>this.p = 1 : 1 +>this.p : number | undefined +>this : this +>p : number | undefined +>1 : 1 + } +} +class Derived extends Base { +>Derived : Derived +>Base : Base + + constructor() { + super(); +>super() : void +>super : typeof Base + + // should be OK, and p should have type number from this assignment + this.p = 1 +>this.p = 1 : 1 +>this.p : number +>this : this +>p : number +>1 : 1 + } + test() { +>test : () => number + + return this.p +>this.p : number +>this : this +>p : number + } +} + diff --git a/tests/cases/conformance/salsa/inferringClassMembersFromAssignments3.ts b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments3.ts new file mode 100644 index 0000000000000..800bd368dba40 --- /dev/null +++ b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments3.ts @@ -0,0 +1,16 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @noImplicitAny: true +// @strictNullChecks: true +// @Filename: a.js +class Base { + constructor() { + this.p = 1 + } +} +class Derived extends Base { + m() { + this.p = 1 + } +} diff --git a/tests/cases/conformance/salsa/inferringClassMembersFromAssignments4.ts b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments4.ts new file mode 100644 index 0000000000000..ba443bf859541 --- /dev/null +++ b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments4.ts @@ -0,0 +1,17 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @noImplicitAny: true +// @strictNullChecks: true +// @Filename: a.js +class Base { + m() { + this.p = 1 + } +} +class Derived extends Base { + m() { + // should be OK, and p should have type number | undefined from its base + this.p = 1 + } +} diff --git a/tests/cases/conformance/salsa/inferringClassMembersFromAssignments5.ts b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments5.ts new file mode 100644 index 0000000000000..5044ea396eb4f --- /dev/null +++ b/tests/cases/conformance/salsa/inferringClassMembersFromAssignments5.ts @@ -0,0 +1,21 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @noImplicitAny: true +// @strictNullChecks: true +// @Filename: a.js +class Base { + m() { + this.p = 1 + } +} +class Derived extends Base { + constructor() { + super(); + // should be OK, and p should have type number from this assignment + this.p = 1 + } + test() { + return this.p + } +}