Skip to content
Prev Previous commit
Next Next commit
Auto-typing for declared properties with no type annotation or initia…
…lizer
  • Loading branch information
ahejlsberg committed Apr 21, 2020
commit fa6fee8ca41663a27774d4a17e114672af58f030
64 changes: 36 additions & 28 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7484,6 +7484,15 @@ namespace ts {
return addOptionality(type, isOptional);
}

if (noImplicitAny && isPropertyDeclaration(declaration)) {
// We are in noImplicitAny mode and have a property declaration with no type annotation or initializer. Use
// control flow analysis of this.xxx assignments the constructor to determine the type of the property.
const constructor = findConstructorDeclaration(declaration.parent);
return constructor ? getFlowTypeInConstructor(declaration.symbol, constructor) :
getModifierFlags(declaration) & ModifierFlags.Ambient ? getTypeOfPropertyInBaseClass(declaration.symbol) :
undefined;
}

if (isJsxAttribute(declaration)) {
// if JSX attribute doesn't have initializer, by default the attribute will have boolean value of true.
// I.e <Elem attr /> is sugar for <Elem attr={true} />
Expand Down Expand Up @@ -7512,13 +7521,18 @@ namespace ts {
getAssignmentDeclarationKind(declaration) === AssignmentDeclarationKind.ThisProperty &&
(declaration.left.kind !== SyntaxKind.ElementAccessExpression || isStringOrNumericLiteralLike((<ElementAccessExpression>declaration.left).argumentExpression)) &&
!getAnnotatedTypeForAssignmentDeclaration(/*declaredType*/ undefined, declaration, symbol, declaration));
links.typeOfPropertyInBaseClass = getTypeOfAssignmentDeclarationPropertyOfBaseType(symbol);
}
return links.isConstructorDeclaredProperty;
}
return false;
}

function isAutoTypedProperty(symbol: Symbol) {
// A property is auto-typed in noImplicitAny mode when its declaration has no type annotation or initializer.
const declaration = symbol.valueDeclaration;
return noImplicitAny && declaration && isPropertyDeclaration(declaration) && !declaration.type && !declaration.initializer;
}

function getDeclaringConstructor(symbol: Symbol) {
for (const declaration of symbol.declarations) {
const container = getThisContainer(declaration, /*includeArrowFunctions*/ false);
Expand All @@ -7528,17 +7542,22 @@ namespace ts {
}
}

function getTypeOfConstructorDeclaredProperty(symbol: Symbol) {
const constructor = getDeclaringConstructor(symbol)!;
function getFlowTypeInConstructor(symbol: Symbol, constructor: ConstructorDeclaration) {
const reference = createPropertyAccess(createThis(), unescapeLeadingUnderscores(symbol.escapedName));
reference.expression.parent = reference;
reference.parent = constructor;
reference.flowNode = constructor.returnFlowNode;
const flowType = getFlowTypeOfReference(reference, autoType, getSymbolLinks(symbol).typeOfPropertyInBaseClass || undefinedType);
const flowType = getFlowTypeOfProperty(reference, symbol);
if (noImplicitAny && (flowType === autoType || flowType === autoArrayType)) {
error(symbol.valueDeclaration, Diagnostics.Member_0_implicitly_has_an_1_type, symbolToString(symbol), typeToString(flowType));
}
return convertAutoToAny(flowType);
// We don't infer a type if assignments are only null or undefined.
return everyType(flowType, isNullableType) ? undefined : convertAutoToAny(flowType);
}

function getFlowTypeOfProperty(reference: Node, prop: Symbol | undefined) {
const initialType = prop && (!isAutoTypedProperty(prop) || getModifierFlags(prop.valueDeclaration) & ModifierFlags.Ambient) && getTypeOfPropertyInBaseClass(prop) || undefinedType;
return getFlowTypeOfReference(reference, autoType, initialType);
}

function getWidenedTypeForAssignmentDeclaration(symbol: Symbol, resolvedSymbol?: Symbol) {
Expand All @@ -7558,10 +7577,7 @@ namespace ts {
// We use control flow analysis to determine the type of the property if the property qualifies as a constructor
// declared property and the resulting control flow type isn't just undefined or null.
if (isConstructorDeclaredProperty(symbol)) {
const controlFlowType = getTypeOfConstructorDeclaredProperty(symbol);
if (!everyType(controlFlowType, isNullableType)) {
type = controlFlowType;
}
type = getFlowTypeInConstructor(symbol, getDeclaringConstructor(symbol)!);
}
if (!type) {
let jsdocType: Type | undefined;
Expand Down Expand Up @@ -7600,7 +7616,7 @@ namespace ts {
let constructorTypes = definedInConstructor ? getConstructorDefinedThisAssignmentTypes(types!, symbol.declarations) : undefined;
// use only the constructor types unless they were only assigned null | undefined (including widening variants)
if (definedInMethod) {
const propType = getTypeOfAssignmentDeclarationPropertyOfBaseType(symbol);
const propType = getTypeOfPropertyInBaseClass(symbol);
if (propType) {
(constructorTypes || (constructorTypes = [])).push(propType);
definedInConstructor = true;
Expand Down Expand Up @@ -7753,21 +7769,6 @@ namespace ts {
});
}

/** check for definition in base class if any declaration is in a class */
function getTypeOfAssignmentDeclarationPropertyOfBaseType(property: Symbol) {
const parentDeclaration = forEach(property.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, property.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
// pattern. Otherwise, it is the type any.
Expand Down Expand Up @@ -17339,7 +17340,14 @@ namespace ts {

// Return the declaring class type of a property or undefined if property not declared in class
function getDeclaringClass(prop: Symbol) {
return prop.parent && prop.parent.flags & SymbolFlags.Class ? getDeclaredTypeOfSymbol(getParentOfSymbol(prop)!) : undefined;
return prop.parent && prop.parent.flags & SymbolFlags.Class ? <InterfaceType>getDeclaredTypeOfSymbol(getParentOfSymbol(prop)!) : undefined;
}

// Return the inherited type of the given property or undefined if property doesn't exist in a base class.
function getTypeOfPropertyInBaseClass(property: Symbol) {
const classType = getDeclaringClass(property);
const baseClassType = classType && getBaseTypes(classType)[0];
return baseClassType && getTypeOfPropertyOfType(baseClassType, property.escapedName);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thinking of mixins, shouldn't we iterate through the base types list and get the property from the first base type which has it, rather than only checking the first base type?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No, classes never have more than one base class, mixins or not.

// Return true if some underlying source property is declared in a class that derives
Expand Down Expand Up @@ -24087,7 +24095,7 @@ namespace ts {
}

function isThisPropertyAccessInConstructor(node: ElementAccessExpression | PropertyAccessExpression | QualifiedName, prop: Symbol) {
return isThisProperty(node) && isConstructorDeclaredProperty(prop) && getThisContainer(node, /*includeArrowFunctions*/ true) === getDeclaringConstructor(prop);
return isThisProperty(node) && (isAutoTypedProperty(prop) || isConstructorDeclaredProperty(prop)) && getThisContainer(node, /*includeArrowFunctions*/ true) === getDeclaringConstructor(prop);
}

function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, leftType: Type, right: Identifier | PrivateIdentifier) {
Expand Down Expand Up @@ -24180,7 +24188,7 @@ namespace ts {
return propType;
}
if (propType === autoType) {
return getFlowTypeOfReference(node, autoType, prop && getSymbolLinks(prop).typeOfPropertyInBaseClass || undefinedType);
return getFlowTypeOfProperty(node, prop);
}
// If strict null checks and strict property initialization checks are enabled, if we have
// a this.xxx property access, if the property is an instance property without an initializer,
Expand Down
1 change: 0 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4157,7 +4157,6 @@ namespace ts {
cjsExportMerged?: Symbol; // Version of the symbol with all non export= exports merged with the export= target
typeOnlyDeclaration?: TypeOnlyCompatibleAliasDeclaration | false; // First resolved alias declaration that makes the symbol only usable in type constructs
isConstructorDeclaredProperty?: boolean; // Property declared through 'this.x = ...' assignment in constructor
typeOfPropertyInBaseClass?: Type; // Type of constructor declared property in base class
}

/* @internal */
Expand Down