Skip to content

Commit 29ca93b

Browse files
authored
Classes can extend Javascript constructor functions (microsoft#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
1 parent 62e6e6a commit 29ca93b

File tree

5 files changed

+678
-21
lines changed

5 files changed

+678
-21
lines changed

src/compiler/checker.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3291,7 +3291,7 @@ namespace ts {
32913291
if (symbol) {
32923292
const isConstructorObject = getObjectFlags(type) & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & SymbolFlags.Class;
32933293
id = (isConstructorObject ? "+" : "") + getSymbolId(symbol);
3294-
if (isJavaScriptConstructor(symbol.valueDeclaration)) {
3294+
if (isJavascriptConstructor(symbol.valueDeclaration)) {
32953295
// Instance and static types share the same symbol; only add 'typeof' for the static side.
32963296
const isInstanceType = type === getInferredClassType(symbol) ? SymbolFlags.Type : SymbolFlags.Value;
32973297
return symbolToTypeNode(symbol, context, isInstanceType);
@@ -5501,7 +5501,7 @@ namespace ts {
55015501
const constraint = getBaseConstraintOfType(type);
55025502
return !!constraint && isValidBaseType(constraint) && isMixinConstructorType(constraint);
55035503
}
5504-
return false;
5504+
return isJavascriptConstructorType(type);
55055505
}
55065506

55075507
function getBaseTypeNodeOfClass(type: InterfaceType): ExpressionWithTypeArguments | undefined {
@@ -5510,9 +5510,12 @@ namespace ts {
55105510

55115511
function getConstructorsForTypeArguments(type: Type, typeArgumentNodes: ReadonlyArray<TypeNode> | undefined, location: Node): ReadonlyArray<Signature> {
55125512
const typeArgCount = length(typeArgumentNodes);
5513-
const isJavaScript = isInJavaScriptFile(location);
5513+
const isJavascript = isInJavaScriptFile(location);
5514+
if (isJavascriptConstructorType(type) && !typeArgCount) {
5515+
return getSignaturesOfType(type, SignatureKind.Call);
5516+
}
55145517
return filter(getSignaturesOfType(type, SignatureKind.Construct),
5515-
sig => (isJavaScript || typeArgCount >= getMinTypeArgumentCount(sig.typeParameters)) && typeArgCount <= length(sig.typeParameters));
5518+
sig => (isJavascript || typeArgCount >= getMinTypeArgumentCount(sig.typeParameters)) && typeArgCount <= length(sig.typeParameters));
55165519
}
55175520

55185521
function getInstantiatedConstructorsForTypeArguments(type: Type, typeArgumentNodes: ReadonlyArray<TypeNode> | undefined, location: Node): ReadonlyArray<Signature> {
@@ -5603,6 +5606,9 @@ namespace ts {
56035606
else if (baseConstructorType.flags & TypeFlags.Any) {
56045607
baseType = baseConstructorType;
56055608
}
5609+
else if (isJavascriptConstructorType(baseConstructorType) && !baseTypeNode.typeArguments) {
5610+
baseType = getJavascriptClassType(baseConstructorType.symbol) || anyType;
5611+
}
56065612
else {
56075613
// The class derives from a "class-like" constructor function, check that we have at least one construct signature
56085614
// with a matching number of type parameters and use the return type of the first instantiated signature. Elsewhere
@@ -10110,7 +10116,7 @@ namespace ts {
1011010116
}
1011110117
}
1011210118
let outerTypeParameters = getOuterTypeParameters(declaration, /*includeThisTypes*/ true);
10113-
if (isJavaScriptConstructor(declaration)) {
10119+
if (isJavascriptConstructor(declaration)) {
1011410120
const templateTagParameters = getTypeParametersFromDeclaration(declaration as DeclarationWithTypeParameters);
1011510121
outerTypeParameters = addRange(outerTypeParameters, templateTagParameters);
1011610122
}
@@ -10407,7 +10413,7 @@ namespace ts {
1040710413
function getTypeWithoutSignatures(type: Type): Type {
1040810414
if (type.flags & TypeFlags.Object) {
1040910415
const resolved = resolveStructuredTypeMembers(<ObjectType>type);
10410-
if (resolved.constructSignatures.length) {
10416+
if (resolved.constructSignatures.length || resolved.callSignatures.length) {
1041110417
const result = createObjectType(ObjectFlags.Anonymous, type.symbol);
1041210418
result.members = resolved.members;
1041310419
result.properties = resolved.properties;
@@ -10741,13 +10747,13 @@ namespace ts {
1074110747
}
1074210748

1074310749
if (!ignoreReturnTypes) {
10744-
const targetReturnType = (target.declaration && isJavaScriptConstructor(target.declaration)) ?
10745-
getJavaScriptClassType(target.declaration.symbol)! : getReturnTypeOfSignature(target);
10750+
const targetReturnType = (target.declaration && isJavascriptConstructor(target.declaration)) ?
10751+
getJavascriptClassType(target.declaration.symbol)! : getReturnTypeOfSignature(target);
1074610752
if (targetReturnType === voidType) {
1074710753
return result;
1074810754
}
10749-
const sourceReturnType = (source.declaration && isJavaScriptConstructor(source.declaration)) ?
10750-
getJavaScriptClassType(source.declaration.symbol)! : getReturnTypeOfSignature(source);
10755+
const sourceReturnType = (source.declaration && isJavascriptConstructor(source.declaration)) ?
10756+
getJavascriptClassType(source.declaration.symbol)! : getReturnTypeOfSignature(source);
1075110757

1075210758
// The following block preserves behavior forbidding boolean returning functions from being assignable to type guard returning functions
1075310759
const targetTypePredicate = getTypePredicateOfSignature(target);
@@ -12014,8 +12020,8 @@ namespace ts {
1201412020
return Ternary.True;
1201512021
}
1201612022

12017-
const sourceIsJSConstructor = source.symbol && isJavaScriptConstructor(source.symbol.valueDeclaration);
12018-
const targetIsJSConstructor = target.symbol && isJavaScriptConstructor(target.symbol.valueDeclaration);
12023+
const sourceIsJSConstructor = source.symbol && isJavascriptConstructor(source.symbol.valueDeclaration);
12024+
const targetIsJSConstructor = target.symbol && isJavascriptConstructor(target.symbol.valueDeclaration);
1201912025

1202012026
const sourceSignatures = getSignaturesOfType(source, (sourceIsJSConstructor && kind === SignatureKind.Construct) ?
1202112027
SignatureKind.Call : kind);
@@ -15500,7 +15506,7 @@ namespace ts {
1550015506
// * /** @constructor */ var x = function() { ... }
1550115507
else if ((container.kind === SyntaxKind.FunctionExpression || container.kind === SyntaxKind.FunctionDeclaration) &&
1550215508
getJSDocClassTag(container)) {
15503-
const classType = getJavaScriptClassType(container.symbol);
15509+
const classType = getJavascriptClassType(container.symbol);
1550415510
if (classType) {
1550515511
return getFlowTypeOfReference(node, classType);
1550615512
}
@@ -19660,7 +19666,7 @@ namespace ts {
1966019666
const callSignatures = getSignaturesOfType(expressionType, SignatureKind.Call);
1966119667
if (callSignatures.length) {
1966219668
const signature = resolveCall(node, callSignatures, candidatesOutArray, isForSignatureHelp);
19663-
if (signature.declaration && !isJavaScriptConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) {
19669+
if (signature.declaration && !isJavascriptConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) {
1966419670
error(node, Diagnostics.Only_a_void_function_can_be_called_with_the_new_keyword);
1966519671
}
1966619672
if (getThisTypeOfSignature(signature) === voidType) {
@@ -19941,7 +19947,7 @@ namespace ts {
1994119947
* Indicates whether a declaration can be treated as a constructor in a JavaScript
1994219948
* file.
1994319949
*/
19944-
function isJavaScriptConstructor(node: Declaration | undefined): boolean {
19950+
function isJavascriptConstructor(node: Declaration | undefined): boolean {
1994519951
if (node && isInJavaScriptFile(node)) {
1994619952
// If the node has a @class tag, treat it like a constructor.
1994719953
if (getJSDocClassTag(node)) return true;
@@ -19957,14 +19963,22 @@ namespace ts {
1995719963
return false;
1995819964
}
1995919965

19960-
function getJavaScriptClassType(symbol: Symbol): Type | undefined {
19966+
function isJavascriptConstructorType(type: Type) {
19967+
if (type.flags & TypeFlags.Object) {
19968+
const resolved = resolveStructuredTypeMembers(<ObjectType>type);
19969+
return resolved.callSignatures.length === 1 && isJavascriptConstructor(resolved.callSignatures[0].declaration);
19970+
}
19971+
return false;
19972+
}
19973+
19974+
function getJavascriptClassType(symbol: Symbol): Type | undefined {
1996119975
let inferred: Type | undefined;
19962-
if (isJavaScriptConstructor(symbol.valueDeclaration)) {
19976+
if (isJavascriptConstructor(symbol.valueDeclaration)) {
1996319977
inferred = getInferredClassType(symbol);
1996419978
}
1996519979
const assigned = getAssignedClassType(symbol);
1996619980
const valueType = getTypeOfSymbol(symbol);
19967-
if (valueType.symbol && !isInferredClassType(valueType) && isJavaScriptConstructor(valueType.symbol.valueDeclaration)) {
19981+
if (valueType.symbol && !isInferredClassType(valueType) && isJavascriptConstructor(valueType.symbol.valueDeclaration)) {
1996819982
inferred = getInferredClassType(valueType.symbol);
1996919983
}
1997019984
return assigned && inferred ?
@@ -20047,7 +20061,7 @@ namespace ts {
2004720061
if (!funcSymbol && node.expression.kind === SyntaxKind.Identifier) {
2004820062
funcSymbol = getResolvedSymbol(node.expression as Identifier);
2004920063
}
20050-
const type = funcSymbol && getJavaScriptClassType(funcSymbol);
20064+
const type = funcSymbol && getJavascriptClassType(funcSymbol);
2005120065
if (type) {
2005220066
return signature.target ? instantiateType(type, signature.mapper) : type;
2005320067
}
@@ -20655,7 +20669,7 @@ namespace ts {
2065520669
return undefined;
2065620670
}
2065720671
if (strictNullChecks && aggregatedTypes.length && hasReturnWithNoExpression &&
20658-
!(isJavaScriptConstructor(func) && aggregatedTypes.some(t => t.symbol === func.symbol))) {
20672+
!(isJavascriptConstructor(func) && aggregatedTypes.some(t => t.symbol === func.symbol))) {
2065920673
// Javascript "callable constructors", containing eg `if (!(this instanceof A)) return new A()` should not add undefined
2066020674
pushIfUnique(aggregatedTypes, undefinedType);
2066120675
}
@@ -25561,8 +25575,9 @@ namespace ts {
2556125575
// that all instantiated base constructor signatures return the same type. We can simply compare the type
2556225576
// references (as opposed to checking the structure of the types) because elsewhere we have already checked
2556325577
// that the base type is a class or interface type (and not, for example, an anonymous object type).
25578+
// (Javascript constructor functions have this property trivially true since their return type is ignored.)
2556425579
const constructors = getInstantiatedConstructorsForTypeArguments(staticBaseType, baseTypeNode.typeArguments, baseTypeNode);
25565-
if (forEach(constructors, sig => getReturnTypeOfSignature(sig) !== baseType)) {
25580+
if (forEach(constructors, sig => !isJavascriptConstructor(sig.declaration) && getReturnTypeOfSignature(sig) !== baseType)) {
2556625581
error(baseTypeNode.expression, Diagnostics.Base_constructors_must_all_have_the_same_return_type);
2556725582
}
2556825583
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
tests/cases/conformance/salsa/first.js(18,9): error TS2554: Expected 1 arguments, but got 0.
2+
tests/cases/conformance/salsa/first.js(26,5): error TS2416: Property 'load' in type 'Sql' is not assignable to the same property in base type 'Wagon'.
3+
Type '(files: string[], format: "csv" | "json" | "xmlolololol") => void' is not assignable to type '(supplies?: any[]) => void'.
4+
tests/cases/conformance/salsa/first.js(36,24): error TS2507: Type '(numberEaten: number) => void' is not a constructor function type.
5+
tests/cases/conformance/salsa/generic.js(8,15): error TS2508: No base constructor has the specified number of type arguments.
6+
tests/cases/conformance/salsa/generic.js(11,21): error TS2339: Property 'flavour' does not exist on type 'Chowder'.
7+
tests/cases/conformance/salsa/generic.js(18,9): error TS2339: Property 'flavour' does not exist on type 'Chowder'.
8+
tests/cases/conformance/salsa/second.ts(8,25): error TS2507: Type '(numberEaten: number) => void' is not a constructor function type.
9+
tests/cases/conformance/salsa/second.ts(14,7): error TS2417: Class static side 'typeof Conestoga' incorrectly extends base class static side 'typeof Wagon'.
10+
Types of property 'circle' are incompatible.
11+
Type '(others: (typeof Wagon)[]) => number' is not assignable to type '(wagons?: Wagon[]) => number'.
12+
Types of parameters 'others' and 'wagons' are incompatible.
13+
Type 'Wagon[]' is not assignable to type '(typeof Wagon)[]'.
14+
Type 'Wagon' is not assignable to type 'typeof Wagon'.
15+
Property 'circle' is missing in type 'Wagon'.
16+
tests/cases/conformance/salsa/second.ts(17,15): error TS2345: Argument of type '"nope"' is not assignable to parameter of type 'number'.
17+
18+
19+
==== tests/cases/conformance/salsa/first.js (3 errors) ====
20+
/**
21+
* @constructor
22+
* @param {number} numberOxen
23+
*/
24+
function Wagon(numberOxen) {
25+
this.numberOxen = numberOxen
26+
}
27+
/** @param {Wagon[]=} wagons */
28+
Wagon.circle = function (wagons) {
29+
return wagons ? wagons.length : 3.14;
30+
}
31+
/** @param {*[]=} supplies - *[]= is my favourite type */
32+
Wagon.prototype.load = function (supplies) {
33+
}
34+
// ok
35+
class Sql extends Wagon {
36+
constructor() {
37+
super(); // error: not enough arguments
38+
~~~~~~~
39+
!!! error TS2554: Expected 1 arguments, but got 0.
40+
this.foonly = 12
41+
}
42+
/**
43+
* @param {Array.<string>} files
44+
* @param {"csv" | "json" | "xmlolololol"} format
45+
* This is not assignable, so should have a type error
46+
*/
47+
load(files, format) {
48+
~~~~
49+
!!! error TS2416: Property 'load' in type 'Sql' is not assignable to the same property in base type 'Wagon'.
50+
!!! error TS2416: Type '(files: string[], format: "csv" | "json" | "xmlolololol") => void' is not assignable to type '(supplies?: any[]) => void'.
51+
if (format === "xmlolololol") {
52+
throw new Error("please do not use XML. It was a joke.");
53+
}
54+
}
55+
}
56+
var db = new Sql();
57+
db.numberOxen = db.foonly
58+
59+
// error, can't extend a TS constructor function
60+
class Drakkhen extends Dragon {
61+
~~~~~~
62+
!!! error TS2507: Type '(numberEaten: number) => void' is not a constructor function type.
63+
64+
}
65+
66+
==== tests/cases/conformance/salsa/second.ts (3 errors) ====
67+
/**
68+
* @constructor
69+
*/
70+
function Dragon(numberEaten: number) {
71+
this.numberEaten = numberEaten
72+
}
73+
// error!
74+
class Firedrake extends Dragon {
75+
~~~~~~
76+
!!! error TS2507: Type '(numberEaten: number) => void' is not a constructor function type.
77+
constructor() {
78+
super();
79+
}
80+
}
81+
// ok
82+
class Conestoga extends Wagon {
83+
~~~~~~~~~
84+
!!! error TS2417: Class static side 'typeof Conestoga' incorrectly extends base class static side 'typeof Wagon'.
85+
!!! error TS2417: Types of property 'circle' are incompatible.
86+
!!! error TS2417: Type '(others: (typeof Wagon)[]) => number' is not assignable to type '(wagons?: Wagon[]) => number'.
87+
!!! error TS2417: Types of parameters 'others' and 'wagons' are incompatible.
88+
!!! error TS2417: Type 'Wagon[]' is not assignable to type '(typeof Wagon)[]'.
89+
!!! error TS2417: Type 'Wagon' is not assignable to type 'typeof Wagon'.
90+
!!! error TS2417: Property 'circle' is missing in type 'Wagon'.
91+
constructor(public drunkOO: true) {
92+
// error: wrong type
93+
super('nope');
94+
~~~~~~
95+
!!! error TS2345: Argument of type '"nope"' is not assignable to parameter of type 'number'.
96+
}
97+
// should error since others is not optional
98+
static circle(others: (typeof Wagon)[]) {
99+
return others.length
100+
}
101+
}
102+
var c = new Conestoga(true);
103+
c.drunkOO
104+
c.numberOxen
105+
106+
==== tests/cases/conformance/salsa/generic.js (3 errors) ====
107+
/**
108+
* @template T
109+
* @param {T} flavour
110+
*/
111+
function Soup(flavour) {
112+
this.flavour = flavour
113+
}
114+
/** @extends {Soup<{ claim: "ignorant" | "malicious" }>} */
115+
~~~~
116+
!!! error TS2508: No base constructor has the specified number of type arguments.
117+
class Chowder extends Soup {
118+
log() {
119+
return this.flavour
120+
~~~~~~~
121+
!!! error TS2339: Property 'flavour' does not exist on type 'Chowder'.
122+
}
123+
}
124+
125+
var soup = new Soup(1);
126+
soup.flavour
127+
var chowder = new Chowder();
128+
chowder.flavour.claim
129+
~~~~~~~
130+
!!! error TS2339: Property 'flavour' does not exist on type 'Chowder'.
131+
132+

0 commit comments

Comments
 (0)