Classes can extend Javascript constructor functions (#26452)

* Classes can extend JS constructor functions

Now ES6 classes can extend ES5 constructor functions, although only
those written in a JS file.

Note that the static side assignability is checked. I need to write
tests to make sure that instance side assignability is checked too.
I haven't tested generic constructor functions yet either.

* Test static+instance assignability errors+generics

Note that generics do not work.

* Cleanup from PR comments

* Even more cleanup

* Update case of function name
This commit is contained in:
Nathan Shively-Sanders
2018-08-14 14:43:04 -07:00
committed by GitHub
parent 62e6e6ae27
commit 29ca93ba48
5 changed files with 678 additions and 21 deletions

View File

@@ -3291,7 +3291,7 @@ namespace ts {
if (symbol) {
const isConstructorObject = getObjectFlags(type) & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & SymbolFlags.Class;
id = (isConstructorObject ? "+" : "") + getSymbolId(symbol);
if (isJavaScriptConstructor(symbol.valueDeclaration)) {
if (isJavascriptConstructor(symbol.valueDeclaration)) {
// Instance and static types share the same symbol; only add 'typeof' for the static side.
const isInstanceType = type === getInferredClassType(symbol) ? SymbolFlags.Type : SymbolFlags.Value;
return symbolToTypeNode(symbol, context, isInstanceType);
@@ -5501,7 +5501,7 @@ namespace ts {
const constraint = getBaseConstraintOfType(type);
return !!constraint && isValidBaseType(constraint) && isMixinConstructorType(constraint);
}
return false;
return isJavascriptConstructorType(type);
}
function getBaseTypeNodeOfClass(type: InterfaceType): ExpressionWithTypeArguments | undefined {
@@ -5510,9 +5510,12 @@ namespace ts {
function getConstructorsForTypeArguments(type: Type, typeArgumentNodes: ReadonlyArray<TypeNode> | undefined, location: Node): ReadonlyArray<Signature> {
const typeArgCount = length(typeArgumentNodes);
const isJavaScript = isInJavaScriptFile(location);
const isJavascript = isInJavaScriptFile(location);
if (isJavascriptConstructorType(type) && !typeArgCount) {
return getSignaturesOfType(type, SignatureKind.Call);
}
return filter(getSignaturesOfType(type, SignatureKind.Construct),
sig => (isJavaScript || typeArgCount >= getMinTypeArgumentCount(sig.typeParameters)) && typeArgCount <= length(sig.typeParameters));
sig => (isJavascript || typeArgCount >= getMinTypeArgumentCount(sig.typeParameters)) && typeArgCount <= length(sig.typeParameters));
}
function getInstantiatedConstructorsForTypeArguments(type: Type, typeArgumentNodes: ReadonlyArray<TypeNode> | undefined, location: Node): ReadonlyArray<Signature> {
@@ -5603,6 +5606,9 @@ namespace ts {
else if (baseConstructorType.flags & TypeFlags.Any) {
baseType = baseConstructorType;
}
else if (isJavascriptConstructorType(baseConstructorType) && !baseTypeNode.typeArguments) {
baseType = getJavascriptClassType(baseConstructorType.symbol) || anyType;
}
else {
// The class derives from a "class-like" constructor function, check that we have at least one construct signature
// with a matching number of type parameters and use the return type of the first instantiated signature. Elsewhere
@@ -10110,7 +10116,7 @@ namespace ts {
}
}
let outerTypeParameters = getOuterTypeParameters(declaration, /*includeThisTypes*/ true);
if (isJavaScriptConstructor(declaration)) {
if (isJavascriptConstructor(declaration)) {
const templateTagParameters = getTypeParametersFromDeclaration(declaration as DeclarationWithTypeParameters);
outerTypeParameters = addRange(outerTypeParameters, templateTagParameters);
}
@@ -10407,7 +10413,7 @@ namespace ts {
function getTypeWithoutSignatures(type: Type): Type {
if (type.flags & TypeFlags.Object) {
const resolved = resolveStructuredTypeMembers(<ObjectType>type);
if (resolved.constructSignatures.length) {
if (resolved.constructSignatures.length || resolved.callSignatures.length) {
const result = createObjectType(ObjectFlags.Anonymous, type.symbol);
result.members = resolved.members;
result.properties = resolved.properties;
@@ -10741,13 +10747,13 @@ namespace ts {
}
if (!ignoreReturnTypes) {
const targetReturnType = (target.declaration && isJavaScriptConstructor(target.declaration)) ?
getJavaScriptClassType(target.declaration.symbol)! : getReturnTypeOfSignature(target);
const targetReturnType = (target.declaration && isJavascriptConstructor(target.declaration)) ?
getJavascriptClassType(target.declaration.symbol)! : getReturnTypeOfSignature(target);
if (targetReturnType === voidType) {
return result;
}
const sourceReturnType = (source.declaration && isJavaScriptConstructor(source.declaration)) ?
getJavaScriptClassType(source.declaration.symbol)! : getReturnTypeOfSignature(source);
const sourceReturnType = (source.declaration && isJavascriptConstructor(source.declaration)) ?
getJavascriptClassType(source.declaration.symbol)! : getReturnTypeOfSignature(source);
// The following block preserves behavior forbidding boolean returning functions from being assignable to type guard returning functions
const targetTypePredicate = getTypePredicateOfSignature(target);
@@ -12014,8 +12020,8 @@ namespace ts {
return Ternary.True;
}
const sourceIsJSConstructor = source.symbol && isJavaScriptConstructor(source.symbol.valueDeclaration);
const targetIsJSConstructor = target.symbol && isJavaScriptConstructor(target.symbol.valueDeclaration);
const sourceIsJSConstructor = source.symbol && isJavascriptConstructor(source.symbol.valueDeclaration);
const targetIsJSConstructor = target.symbol && isJavascriptConstructor(target.symbol.valueDeclaration);
const sourceSignatures = getSignaturesOfType(source, (sourceIsJSConstructor && kind === SignatureKind.Construct) ?
SignatureKind.Call : kind);
@@ -15500,7 +15506,7 @@ namespace ts {
// * /** @constructor */ var x = function() { ... }
else if ((container.kind === SyntaxKind.FunctionExpression || container.kind === SyntaxKind.FunctionDeclaration) &&
getJSDocClassTag(container)) {
const classType = getJavaScriptClassType(container.symbol);
const classType = getJavascriptClassType(container.symbol);
if (classType) {
return getFlowTypeOfReference(node, classType);
}
@@ -19660,7 +19666,7 @@ namespace ts {
const callSignatures = getSignaturesOfType(expressionType, SignatureKind.Call);
if (callSignatures.length) {
const signature = resolveCall(node, callSignatures, candidatesOutArray, isForSignatureHelp);
if (signature.declaration && !isJavaScriptConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) {
if (signature.declaration && !isJavascriptConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) {
error(node, Diagnostics.Only_a_void_function_can_be_called_with_the_new_keyword);
}
if (getThisTypeOfSignature(signature) === voidType) {
@@ -19941,7 +19947,7 @@ namespace ts {
* Indicates whether a declaration can be treated as a constructor in a JavaScript
* file.
*/
function isJavaScriptConstructor(node: Declaration | undefined): boolean {
function isJavascriptConstructor(node: Declaration | undefined): boolean {
if (node && isInJavaScriptFile(node)) {
// If the node has a @class tag, treat it like a constructor.
if (getJSDocClassTag(node)) return true;
@@ -19957,14 +19963,22 @@ namespace ts {
return false;
}
function getJavaScriptClassType(symbol: Symbol): Type | undefined {
function isJavascriptConstructorType(type: Type) {
if (type.flags & TypeFlags.Object) {
const resolved = resolveStructuredTypeMembers(<ObjectType>type);
return resolved.callSignatures.length === 1 && isJavascriptConstructor(resolved.callSignatures[0].declaration);
}
return false;
}
function getJavascriptClassType(symbol: Symbol): Type | undefined {
let inferred: Type | undefined;
if (isJavaScriptConstructor(symbol.valueDeclaration)) {
if (isJavascriptConstructor(symbol.valueDeclaration)) {
inferred = getInferredClassType(symbol);
}
const assigned = getAssignedClassType(symbol);
const valueType = getTypeOfSymbol(symbol);
if (valueType.symbol && !isInferredClassType(valueType) && isJavaScriptConstructor(valueType.symbol.valueDeclaration)) {
if (valueType.symbol && !isInferredClassType(valueType) && isJavascriptConstructor(valueType.symbol.valueDeclaration)) {
inferred = getInferredClassType(valueType.symbol);
}
return assigned && inferred ?
@@ -20047,7 +20061,7 @@ namespace ts {
if (!funcSymbol && node.expression.kind === SyntaxKind.Identifier) {
funcSymbol = getResolvedSymbol(node.expression as Identifier);
}
const type = funcSymbol && getJavaScriptClassType(funcSymbol);
const type = funcSymbol && getJavascriptClassType(funcSymbol);
if (type) {
return signature.target ? instantiateType(type, signature.mapper) : type;
}
@@ -20655,7 +20669,7 @@ namespace ts {
return undefined;
}
if (strictNullChecks && aggregatedTypes.length && hasReturnWithNoExpression &&
!(isJavaScriptConstructor(func) && aggregatedTypes.some(t => t.symbol === func.symbol))) {
!(isJavascriptConstructor(func) && aggregatedTypes.some(t => t.symbol === func.symbol))) {
// Javascript "callable constructors", containing eg `if (!(this instanceof A)) return new A()` should not add undefined
pushIfUnique(aggregatedTypes, undefinedType);
}
@@ -25561,8 +25575,9 @@ namespace ts {
// that all instantiated base constructor signatures return the same type. We can simply compare the type
// references (as opposed to checking the structure of the types) because elsewhere we have already checked
// that the base type is a class or interface type (and not, for example, an anonymous object type).
// (Javascript constructor functions have this property trivially true since their return type is ignored.)
const constructors = getInstantiatedConstructorsForTypeArguments(staticBaseType, baseTypeNode.typeArguments, baseTypeNode);
if (forEach(constructors, sig => getReturnTypeOfSignature(sig) !== baseType)) {
if (forEach(constructors, sig => !isJavascriptConstructor(sig.declaration) && getReturnTypeOfSignature(sig) !== baseType)) {
error(baseTypeNode.expression, Diagnostics.Base_constructors_must_all_have_the_same_return_type);
}
}