Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support parameter types for mixin constructors #28232

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 43 additions & 51 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5634,15 +5634,23 @@ namespace ts {
return concatenate(getOuterTypeParametersOfClassOrInterface(symbol), getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol));
}

// A type is a mixin constructor if it has a single construct signature taking no type parameters and a single
// rest parameter of type any[].
// A type is a mixin constructor if it has all construct signatures taking no type parameters
// and the same parameter types.
function isMixinConstructorType(type: Type) {
const signatures = getSignaturesOfType(type, SignatureKind.Construct);
if (signatures.length === 1) {
const s = signatures[0];
return !s.typeParameters && s.parameters.length === 1 && s.hasRestParameter && getTypeOfParameter(s.parameters[0]) === anyArrayType;
if (signatures.length === 0) {
return false;
}
return false;
for (let i = 0; i < signatures.length; ++i) {
const signature = signatures[i];
if (signature.typeParameters) {
return false;
}
if ((i !== 0) && !compareSignaturesIdentical(signature, signatures[0], /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false, compareTypesIdentical)) {
return false;
}
}
return true;
}

function isConstructorType(type: Type): boolean {
Expand All @@ -5651,7 +5659,7 @@ namespace ts {
}
if (type.flags & TypeFlags.TypeVariable) {
const constraint = getBaseConstraintOfType(type);
return !!constraint && isValidBaseType(constraint) && isMixinConstructorType(constraint);
return !!constraint && isConstructorType(constraint);
}
return isJSConstructorType(type);
}
Expand Down Expand Up @@ -6672,19 +6680,6 @@ namespace ts {
getUnionType([info1.type, info2.type]), info1.isReadonly || info2.isReadonly);
}

function includeMixinType(type: Type, types: ReadonlyArray<Type>, index: number): Type {
const mixedTypes: Type[] = [];
for (let i = 0; i < types.length; i++) {
if (i === index) {
mixedTypes.push(type);
}
else if (isMixinConstructorType(types[i])) {
mixedTypes.push(getReturnTypeOfSignature(getSignaturesOfType(types[i], SignatureKind.Construct)[0]));
}
}
return getIntersectionType(mixedTypes);
}

function resolveIntersectionTypeMembers(type: IntersectionType) {
// The members and properties collections are empty for intersection types. To get all properties of an
// intersection type use getPropertiesOfType (only the language service uses this).
Expand All @@ -6693,29 +6688,33 @@ namespace ts {
let stringIndexInfo: IndexInfo | undefined;
let numberIndexInfo: IndexInfo | undefined;
const types = type.types;
const mixinCount = countWhere(types, isMixinConstructorType);
for (let i = 0; i < types.length; i++) {
const t = type.types[i];
// When an intersection type contains mixin constructor types, the construct signatures from
// those types are discarded and their return types are mixed into the return types of all
// other construct signatures in the intersection type. For example, the intersection type
// '{ new(...args: any[]) => A } & { new(s: string) => B }' has a single construct signature
// 'new(s: string) => A & B'.
if (mixinCount === 0 || mixinCount === types.length && i === 0 || !isMixinConstructorType(t)) {
let signatures = getSignaturesOfType(t, SignatureKind.Construct);
if (signatures.length && mixinCount > 0) {
signatures = map(signatures, s => {
const clone = cloneSignature(s);
clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, i);
return clone;
});
}
constructSignatures = concatenate(constructSignatures, signatures);
}
constructSignatures = concatenate(constructSignatures, getSignaturesOfType(t, SignatureKind.Construct));
callSignatures = concatenate(callSignatures, getSignaturesOfType(t, SignatureKind.Call));
stringIndexInfo = intersectIndexInfos(stringIndexInfo, getIndexInfoOfType(t, IndexKind.String));
numberIndexInfo = intersectIndexInfos(numberIndexInfo, getIndexInfoOfType(t, IndexKind.Number));
}

if (constructSignatures.length) {
// When an intersection type contains constructor types, the construct signature return types
// are replaced with the single intersection of all return types.
// For example, the intersection type
// '{ new(s: string) => A } & { new(s: string) => B }' has a single construct signature
// 'new(s: string) => A & B'.
const eq = (s0: Signature, s1: Signature) => compareSignaturesIdentical(s0, s1, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true, compareTypesIdentical) === Ternary.True;
const returnType = getIntersectionType(constructSignatures.map(s => getReturnTypeOfSignature(s)));
const uniqConstructSignatures = Array<Signature>();
for (const signature of constructSignatures) {
if (!contains(uniqConstructSignatures, signature, eq)) {
const clone = cloneSignature(signature);
clone.resolvedReturnType = returnType;
uniqConstructSignatures.push(clone);
}
}
constructSignatures = uniqConstructSignatures;
}

setStructuredTypeMembers(type, emptySymbols, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo);
}

Expand Down Expand Up @@ -20004,20 +20003,13 @@ namespace ts {
}
const firstBase = baseTypes[0];
if (firstBase.flags & TypeFlags.Intersection) {
const types = (firstBase as IntersectionType).types;
const mixinCount = countWhere(types, isMixinConstructorType);
let i = 0;
for (const intersectionMember of (firstBase as IntersectionType).types) {
i++;
// We want to ignore mixin ctors
if (mixinCount === 0 || mixinCount === types.length && i === 0 || !isMixinConstructorType(intersectionMember)) {
if (getObjectFlags(intersectionMember) & (ObjectFlags.Class | ObjectFlags.Interface)) {
if (intersectionMember.symbol === target) {
return true;
}
if (typeHasProtectedAccessibleBase(target, intersectionMember as InterfaceType)) {
return true;
}
if (getObjectFlags(intersectionMember) & (ObjectFlags.Class | ObjectFlags.Interface)) {
if (intersectionMember.symbol === target) {
return true;
}
if (typeHasProtectedAccessibleBase(target, intersectionMember as InterfaceType)) {
return true;
}
}
}
Expand Down Expand Up @@ -26017,7 +26009,7 @@ namespace ts {
Diagnostics.Class_static_side_0_incorrectly_extends_base_class_static_side_1);
}
if (baseConstructorType.flags & TypeFlags.TypeVariable && !isMixinConstructorType(staticType)) {
error(node.name || node, Diagnostics.A_mixin_class_must_have_a_constructor_with_a_single_rest_parameter_of_type_any);
error(node.name || node, Diagnostics.A_mixin_class_must_have_a_constructor_with_the_same_parameter_types_as_the_base_class);
}

if (!(staticBaseType.symbol && staticBaseType.symbol.flags & SymbolFlags.Class) && !(baseConstructorType.flags & TypeFlags.TypeVariable)) {
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1960,7 +1960,7 @@
"category": "Error",
"code": 2544
},
"A mixin class must have a constructor with a single rest parameter of type 'any[]'.": {
"A mixin class must have a constructor with the same parameter types as the base class.": {
"category": "Error",
"code": 2545
},
Expand Down
73 changes: 73 additions & 0 deletions tests/baselines/reference/genericWildcardBaseClass.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
tests/cases/compiler/genericWildcardBaseClass.ts(23,18): error TS2545: A mixin class must have a constructor with the same parameter types as the base class.
tests/cases/compiler/genericWildcardBaseClass.ts(42,49): error TS2345: Argument of type 'typeof BadClass' is not assignable to parameter of type 'typeof BaseClass'.
Types of parameters 'n' and 's' are incompatible.
Type 'string' is not assignable to type 'number'.
tests/cases/compiler/genericWildcardBaseClass.ts(46,30): error TS2345: Argument of type '2' is not assignable to parameter of type 'string'.


==== tests/cases/compiler/genericWildcardBaseClass.ts (3 errors) ====
abstract class BaseClass {
constructor(s: string = '', ...args: any[]) { }
base() { return 0; }
static staticBase() { return ''; }
}

function extendNoConstructor<T extends typeof BaseClass>(Base: T) {
return class ExN extends Base {
ext() { return 0; }
static staticExt() { return ''; }
};
}

function extendCompatibleConstructor<T extends typeof BaseClass>(Base: T) {
return class ExC extends Base {
constructor(x?: string, ...args: any[]) {
super(x, args);
}
};
}

function fails_IncompatibleConstructor<T extends typeof BaseClass>(Base: T) {
return class Fail extends Base {
~~~~
!!! error TS2545: A mixin class must have a constructor with the same parameter types as the base class.
constructor(x?: string, ...args: string[]) {
super(x, args);
}
};
}

abstract class ExtClass extends BaseClass {
thing() { return 0; }
static staticThing() { return ''; }
}

abstract class BadClass extends BaseClass {
constructor(n: number) {
super();
}
}

const Thing2 = extendCompatibleConstructor(extendNoConstructor(ExtClass));
extendCompatibleConstructor(extendNoConstructor(BadClass));
~~~~~~~~
!!! error TS2345: Argument of type 'typeof BadClass' is not assignable to parameter of type 'typeof BaseClass'.
!!! error TS2345: Types of parameters 'n' and 's' are incompatible.
!!! error TS2345: Type 'string' is not assignable to type 'number'.

const thing2 = new Thing2();
const thing2arg = new Thing2('');
const fails_arg = new Thing2(2);
~
!!! error TS2345: Argument of type '2' is not assignable to parameter of type 'string'.

const str2 = Thing2.staticExt() + Thing2.staticThing() + Thing2.staticBase();
const num2 = thing2.ext() + thing2.thing() + thing2.base();

class Thing3 extends Thing2 {
constructor() {
super('', 1, 2);
Math.round(this.base() + this.thing() + this.ext());
}
}

154 changes: 154 additions & 0 deletions tests/baselines/reference/genericWildcardBaseClass.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//// [genericWildcardBaseClass.ts]
abstract class BaseClass {
constructor(s: string = '', ...args: any[]) { }
base() { return 0; }
static staticBase() { return ''; }
}

function extendNoConstructor<T extends typeof BaseClass>(Base: T) {
return class ExN extends Base {
ext() { return 0; }
static staticExt() { return ''; }
};
}

function extendCompatibleConstructor<T extends typeof BaseClass>(Base: T) {
return class ExC extends Base {
constructor(x?: string, ...args: any[]) {
super(x, args);
}
};
}

function fails_IncompatibleConstructor<T extends typeof BaseClass>(Base: T) {
return class Fail extends Base {
constructor(x?: string, ...args: string[]) {
super(x, args);
}
};
}

abstract class ExtClass extends BaseClass {
thing() { return 0; }
static staticThing() { return ''; }
}

abstract class BadClass extends BaseClass {
constructor(n: number) {
super();
}
}

const Thing2 = extendCompatibleConstructor(extendNoConstructor(ExtClass));
extendCompatibleConstructor(extendNoConstructor(BadClass));

const thing2 = new Thing2();
const thing2arg = new Thing2('');
const fails_arg = new Thing2(2);

const str2 = Thing2.staticExt() + Thing2.staticThing() + Thing2.staticBase();
const num2 = thing2.ext() + thing2.thing() + thing2.base();

class Thing3 extends Thing2 {
constructor() {
super('', 1, 2);
Math.round(this.base() + this.thing() + this.ext());
}
}


//// [genericWildcardBaseClass.js]
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var BaseClass = /** @class */ (function () {
function BaseClass(s) {
if (s === void 0) { s = ''; }
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
}
BaseClass.prototype.base = function () { return 0; };
BaseClass.staticBase = function () { return ''; };
return BaseClass;
}());
function extendNoConstructor(Base) {
return /** @class */ (function (_super) {
__extends(ExN, _super);
function ExN() {
return _super !== null && _super.apply(this, arguments) || this;
}
ExN.prototype.ext = function () { return 0; };
ExN.staticExt = function () { return ''; };
return ExN;
}(Base));
}
function extendCompatibleConstructor(Base) {
return /** @class */ (function (_super) {
__extends(ExC, _super);
function ExC(x) {
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
return _super.call(this, x, args) || this;
}
return ExC;
}(Base));
}
function fails_IncompatibleConstructor(Base) {
return /** @class */ (function (_super) {
__extends(Fail, _super);
function Fail(x) {
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
return _super.call(this, x, args) || this;
}
return Fail;
}(Base));
}
var ExtClass = /** @class */ (function (_super) {
__extends(ExtClass, _super);
function ExtClass() {
return _super !== null && _super.apply(this, arguments) || this;
}
ExtClass.prototype.thing = function () { return 0; };
ExtClass.staticThing = function () { return ''; };
return ExtClass;
}(BaseClass));
var BadClass = /** @class */ (function (_super) {
__extends(BadClass, _super);
function BadClass(n) {
return _super.call(this) || this;
}
return BadClass;
}(BaseClass));
var Thing2 = extendCompatibleConstructor(extendNoConstructor(ExtClass));
extendCompatibleConstructor(extendNoConstructor(BadClass));
var thing2 = new Thing2();
var thing2arg = new Thing2('');
var fails_arg = new Thing2(2);
var str2 = Thing2.staticExt() + Thing2.staticThing() + Thing2.staticBase();
var num2 = thing2.ext() + thing2.thing() + thing2.base();
var Thing3 = /** @class */ (function (_super) {
__extends(Thing3, _super);
function Thing3() {
var _this = _super.call(this, '', 1, 2) || this;
Math.round(_this.base() + _this.thing() + _this.ext());
return _this;
}
return Thing3;
}(Thing2));
Loading