Skip to content

Commit f32f95a

Browse files
authored
Merge pull request #13743 from Microsoft/mixinClasses
Mixin classes
2 parents 9b6f930 + 004dbf4 commit f32f95a

14 files changed

+2298
-14
lines changed

Diff for: src/compiler/checker.ts

+82-14
Original file line numberDiff line numberDiff line change
@@ -2465,7 +2465,8 @@ namespace ts {
24652465
const symbol = type.symbol;
24662466
if (symbol) {
24672467
// Always use 'typeof T' for type of class, enum, and module objects
2468-
if (symbol.flags & (SymbolFlags.Class | SymbolFlags.Enum | SymbolFlags.ValueModule)) {
2468+
if (symbol.flags & SymbolFlags.Class && !getBaseTypeVariableOfClass(symbol) ||
2469+
symbol.flags & (SymbolFlags.Enum | SymbolFlags.ValueModule)) {
24692470
writeTypeOfSymbol(type, flags);
24702471
}
24712472
else if (shouldWriteTypeOfFunctionSymbol()) {
@@ -3639,6 +3640,11 @@ namespace ts {
36393640
return links.type;
36403641
}
36413642

3643+
function getBaseTypeVariableOfClass(symbol: Symbol) {
3644+
const baseConstructorType = getBaseConstructorTypeOfClass(getDeclaredTypeOfClassOrInterface(symbol));
3645+
return baseConstructorType.flags & TypeFlags.TypeVariable ? baseConstructorType : undefined;
3646+
}
3647+
36423648
function getTypeOfFuncClassEnumModule(symbol: Symbol): Type {
36433649
const links = getSymbolLinks(symbol);
36443650
if (!links.type) {
@@ -3647,8 +3653,13 @@ namespace ts {
36473653
}
36483654
else {
36493655
const type = createObjectType(ObjectFlags.Anonymous, symbol);
3650-
links.type = strictNullChecks && symbol.flags & SymbolFlags.Optional ?
3651-
includeFalsyTypes(type, TypeFlags.Undefined) : type;
3656+
if (symbol.flags & SymbolFlags.Class) {
3657+
const baseTypeVariable = getBaseTypeVariableOfClass(symbol);
3658+
links.type = baseTypeVariable ? getIntersectionType([type, baseTypeVariable]) : type;
3659+
}
3660+
else {
3661+
links.type = strictNullChecks && symbol.flags & SymbolFlags.Optional ? includeFalsyTypes(type, TypeFlags.Undefined) : type;
3662+
}
36523663
}
36533664
}
36543665
return links.type;
@@ -3812,8 +3823,26 @@ namespace ts {
38123823
return concatenate(getOuterTypeParametersOfClassOrInterface(symbol), getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol));
38133824
}
38143825

3826+
// A type is a mixin constructor if it has a single construct signature taking no type parameters and a single
3827+
// rest parameter of type any[].
3828+
function isMixinConstructorType(type: Type) {
3829+
const signatures = getSignaturesOfType(type, SignatureKind.Construct);
3830+
if (signatures.length === 1) {
3831+
const s = signatures[0];
3832+
return !s.typeParameters && s.parameters.length === 1 && s.hasRestParameter && getTypeOfParameter(s.parameters[0]) === anyArrayType;
3833+
}
3834+
return false;
3835+
}
3836+
38153837
function isConstructorType(type: Type): boolean {
3816-
return isValidBaseType(type) && getSignaturesOfType(type, SignatureKind.Construct).length > 0;
3838+
if (isValidBaseType(type) && getSignaturesOfType(type, SignatureKind.Construct).length > 0) {
3839+
return true;
3840+
}
3841+
if (type.flags & TypeFlags.TypeVariable) {
3842+
const constraint = getBaseConstraintOfType(<TypeVariable>type);
3843+
return isValidBaseType(constraint) && isMixinConstructorType(constraint);
3844+
}
3845+
return false;
38173846
}
38183847

38193848
function getBaseTypeNodeOfClass(type: InterfaceType): ExpressionWithTypeArguments {
@@ -3892,7 +3921,7 @@ namespace ts {
38923921

38933922
function resolveBaseTypesOfClass(type: InterfaceType): void {
38943923
type.resolvedBaseTypes = type.resolvedBaseTypes || emptyArray;
3895-
const baseConstructorType = <ObjectType>getBaseConstructorTypeOfClass(type);
3924+
const baseConstructorType = getApparentType(getBaseConstructorTypeOfClass(type));
38963925
if (!(baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection))) {
38973926
return;
38983927
}
@@ -4542,16 +4571,47 @@ namespace ts {
45424571
getUnionType([info1.type, info2.type]), info1.isReadonly || info2.isReadonly);
45434572
}
45444573

4574+
function includeMixinType(type: Type, types: Type[], index: number): Type {
4575+
const mixedTypes: Type[] = [];
4576+
for (let i = 0; i < types.length; i++) {
4577+
if (i === index) {
4578+
mixedTypes.push(type);
4579+
}
4580+
else if (isMixinConstructorType(types[i])) {
4581+
mixedTypes.push(getReturnTypeOfSignature(getSignaturesOfType(types[i], SignatureKind.Construct)[0]));
4582+
}
4583+
}
4584+
return getIntersectionType(mixedTypes);
4585+
}
4586+
45454587
function resolveIntersectionTypeMembers(type: IntersectionType) {
45464588
// The members and properties collections are empty for intersection types. To get all properties of an
45474589
// intersection type use getPropertiesOfType (only the language service uses this).
45484590
let callSignatures: Signature[] = emptyArray;
45494591
let constructSignatures: Signature[] = emptyArray;
4550-
let stringIndexInfo: IndexInfo = undefined;
4551-
let numberIndexInfo: IndexInfo = undefined;
4552-
for (const t of type.types) {
4592+
let stringIndexInfo: IndexInfo;
4593+
let numberIndexInfo: IndexInfo;
4594+
const types = type.types;
4595+
const mixinCount = countWhere(types, isMixinConstructorType);
4596+
for (let i = 0; i < types.length; i++) {
4597+
const t = type.types[i];
4598+
// When an intersection type contains mixin constructor types, the construct signatures from
4599+
// those types are discarded and their return types are mixed into the return types of all
4600+
// other construct signatures in the intersection type. For example, the intersection type
4601+
// '{ new(...args: any[]) => A } & { new(s: string) => B }' has a single construct signature
4602+
// 'new(s: string) => A & B'.
4603+
if (mixinCount === 0 || mixinCount === types.length && i === 0 || !isMixinConstructorType(t)) {
4604+
let signatures = getSignaturesOfType(t, SignatureKind.Construct);
4605+
if (signatures.length && mixinCount > 0) {
4606+
signatures = map(signatures, s => {
4607+
const clone = cloneSignature(s);
4608+
clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, i);
4609+
return clone;
4610+
});
4611+
}
4612+
constructSignatures = concatenate(constructSignatures, signatures);
4613+
}
45534614
callSignatures = concatenate(callSignatures, getSignaturesOfType(t, SignatureKind.Call));
4554-
constructSignatures = concatenate(constructSignatures, getSignaturesOfType(t, SignatureKind.Construct));
45554615
stringIndexInfo = intersectIndexInfos(stringIndexInfo, getIndexInfoOfType(t, IndexKind.String));
45564616
numberIndexInfo = intersectIndexInfos(numberIndexInfo, getIndexInfoOfType(t, IndexKind.Number));
45574617
}
@@ -4593,7 +4653,7 @@ namespace ts {
45934653
constructSignatures = getDefaultConstructSignatures(classType);
45944654
}
45954655
const baseConstructorType = getBaseConstructorTypeOfClass(classType);
4596-
if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection)) {
4656+
if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection | TypeFlags.TypeVariable)) {
45974657
members = createSymbolTable(getNamedMembers(members));
45984658
addInheritedMembers(members, getPropertiesOfType(baseConstructorType));
45994659
}
@@ -4890,6 +4950,7 @@ namespace ts {
48904950

48914951
function createUnionOrIntersectionProperty(containingType: UnionOrIntersectionType, name: string): Symbol {
48924952
const types = containingType.types;
4953+
const excludeModifiers = containingType.flags & TypeFlags.Union ? ModifierFlags.Private | ModifierFlags.Protected : 0;
48934954
let props: Symbol[];
48944955
// Flags we want to propagate to the result if they exist in all source symbols
48954956
let commonFlags = (containingType.flags & TypeFlags.Intersection) ? SymbolFlags.Optional : SymbolFlags.None;
@@ -4899,7 +4960,7 @@ namespace ts {
48994960
const type = getApparentType(current);
49004961
if (type !== unknownType) {
49014962
const prop = getPropertyOfType(type, name);
4902-
if (prop && !(getDeclarationModifierFlagsFromSymbol(prop) & (ModifierFlags.Private | ModifierFlags.Protected))) {
4963+
if (prop && !(getDeclarationModifierFlagsFromSymbol(prop) & excludeModifiers)) {
49034964
commonFlags &= prop.flags;
49044965
if (!props) {
49054966
props = [prop];
@@ -6965,9 +7026,12 @@ namespace ts {
69657026
result.properties = resolved.properties;
69667027
result.callSignatures = emptyArray;
69677028
result.constructSignatures = emptyArray;
6968-
type = result;
7029+
return result;
69697030
}
69707031
}
7032+
else if (type.flags & TypeFlags.Intersection) {
7033+
return getIntersectionType(map((<IntersectionType>type).types, getTypeWithoutSignatures));
7034+
}
69717035
return type;
69727036
}
69737037

@@ -18390,7 +18454,8 @@ namespace ts {
1839018454
const baseTypes = getBaseTypes(type);
1839118455
if (baseTypes.length && produceDiagnostics) {
1839218456
const baseType = baseTypes[0];
18393-
const staticBaseType = getBaseConstructorTypeOfClass(type);
18457+
const baseConstructorType = getBaseConstructorTypeOfClass(type);
18458+
const staticBaseType = getApparentType(baseConstructorType);
1839418459
checkBaseTypeAccessibility(staticBaseType, baseTypeNode);
1839518460
checkSourceElement(baseTypeNode.expression);
1839618461
if (baseTypeNode.typeArguments) {
@@ -18404,6 +18469,9 @@ namespace ts {
1840418469
checkTypeAssignableTo(typeWithThis, getTypeWithThisArgument(baseType, type.thisType), node.name || node, Diagnostics.Class_0_incorrectly_extends_base_class_1);
1840518470
checkTypeAssignableTo(staticType, getTypeWithoutSignatures(staticBaseType), node.name || node,
1840618471
Diagnostics.Class_static_side_0_incorrectly_extends_base_class_static_side_1);
18472+
if (baseConstructorType.flags & TypeFlags.TypeVariable && !isMixinConstructorType(staticType)) {
18473+
error(node.name || node, Diagnostics.A_mixin_class_must_have_a_constructor_with_a_single_rest_parameter_of_type_any);
18474+
}
1840718475

1840818476
if (baseType.symbol && baseType.symbol.valueDeclaration &&
1840918477
!isInAmbientContext(baseType.symbol.valueDeclaration) &&
@@ -18413,7 +18481,7 @@ namespace ts {
1841318481
}
1841418482
}
1841518483

18416-
if (!(staticBaseType.symbol && staticBaseType.symbol.flags & SymbolFlags.Class)) {
18484+
if (!(staticBaseType.symbol && staticBaseType.symbol.flags & SymbolFlags.Class) && !(baseConstructorType.flags & TypeFlags.TypeVariable)) {
1841718485
// When the static base type is a "class-like" constructor function (but not actually a class), we verify
1841818486
// that all instantiated base constructor signatures return the same type. We can simply compare the type
1841918487
// references (as opposed to checking the structure of the types) because elsewhere we have already checked

Diff for: src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -1783,6 +1783,10 @@
17831783
"category": "Error",
17841784
"code": 2544
17851785
},
1786+
"A mixin class must have a constructor with a single rest parameter of type 'any[]'.": {
1787+
"category": "Error",
1788+
"code": 2545
1789+
},
17861790
"JSX element attributes type '{0}' may not be a union type.": {
17871791
"category": "Error",
17881792
"code": 2600

Diff for: tests/baselines/reference/mixinClassesAnnotated.js

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
//// [mixinClassesAnnotated.ts]
2+
3+
type Constructor<T> = new(...args: any[]) => T;
4+
5+
class Base {
6+
constructor(public x: number, public y: number) {}
7+
}
8+
9+
class Derived extends Base {
10+
constructor(x: number, y: number, public z: number) {
11+
super(x, y);
12+
}
13+
}
14+
15+
interface Printable {
16+
print(): void;
17+
}
18+
19+
const Printable = <T extends Constructor<Base>>(superClass: T): Constructor<Printable> & { message: string } & T =>
20+
class extends superClass {
21+
static message = "hello";
22+
print() {
23+
const output = this.x + "," + this.y;
24+
}
25+
}
26+
27+
interface Tagged {
28+
_tag: string;
29+
}
30+
31+
function Tagged<T extends Constructor<{}>>(superClass: T): Constructor<Tagged> & T {
32+
class C extends superClass {
33+
_tag: string;
34+
constructor(...args: any[]) {
35+
super(...args);
36+
this._tag = "hello";
37+
}
38+
}
39+
return C;
40+
}
41+
42+
const Thing1 = Tagged(Derived);
43+
const Thing2 = Tagged(Printable(Derived));
44+
Thing2.message;
45+
46+
function f1() {
47+
const thing = new Thing1(1, 2, 3);
48+
thing.x;
49+
thing._tag;
50+
}
51+
52+
function f2() {
53+
const thing = new Thing2(1, 2, 3);
54+
thing.x;
55+
thing._tag;
56+
thing.print();
57+
}
58+
59+
class Thing3 extends Thing2 {
60+
constructor(tag: string) {
61+
super(10, 20, 30);
62+
this._tag = tag;
63+
}
64+
test() {
65+
this.print();
66+
}
67+
}
68+
69+
70+
//// [mixinClassesAnnotated.js]
71+
var __extends = (this && this.__extends) || (function () {
72+
var extendStatics = Object.setPrototypeOf ||
73+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
74+
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
75+
return function (d, b) {
76+
extendStatics(d, b);
77+
function __() { this.constructor = d; }
78+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
79+
};
80+
})();
81+
var Base = (function () {
82+
function Base(x, y) {
83+
this.x = x;
84+
this.y = y;
85+
}
86+
return Base;
87+
}());
88+
var Derived = (function (_super) {
89+
__extends(Derived, _super);
90+
function Derived(x, y, z) {
91+
var _this = _super.call(this, x, y) || this;
92+
_this.z = z;
93+
return _this;
94+
}
95+
return Derived;
96+
}(Base));
97+
var Printable = function (superClass) { return _a = (function (_super) {
98+
__extends(class_1, _super);
99+
function class_1() {
100+
return _super !== null && _super.apply(this, arguments) || this;
101+
}
102+
class_1.prototype.print = function () {
103+
var output = this.x + "," + this.y;
104+
};
105+
return class_1;
106+
}(superClass)),
107+
_a.message = "hello",
108+
_a; var _a; };
109+
function Tagged(superClass) {
110+
var C = (function (_super) {
111+
__extends(C, _super);
112+
function C() {
113+
var args = [];
114+
for (var _i = 0; _i < arguments.length; _i++) {
115+
args[_i] = arguments[_i];
116+
}
117+
var _this = _super.apply(this, args) || this;
118+
_this._tag = "hello";
119+
return _this;
120+
}
121+
return C;
122+
}(superClass));
123+
return C;
124+
}
125+
var Thing1 = Tagged(Derived);
126+
var Thing2 = Tagged(Printable(Derived));
127+
Thing2.message;
128+
function f1() {
129+
var thing = new Thing1(1, 2, 3);
130+
thing.x;
131+
thing._tag;
132+
}
133+
function f2() {
134+
var thing = new Thing2(1, 2, 3);
135+
thing.x;
136+
thing._tag;
137+
thing.print();
138+
}
139+
var Thing3 = (function (_super) {
140+
__extends(Thing3, _super);
141+
function Thing3(tag) {
142+
var _this = _super.call(this, 10, 20, 30) || this;
143+
_this._tag = tag;
144+
return _this;
145+
}
146+
Thing3.prototype.test = function () {
147+
this.print();
148+
};
149+
return Thing3;
150+
}(Thing2));
151+
152+
153+
//// [mixinClassesAnnotated.d.ts]
154+
declare type Constructor<T> = new (...args: any[]) => T;
155+
declare class Base {
156+
x: number;
157+
y: number;
158+
constructor(x: number, y: number);
159+
}
160+
declare class Derived extends Base {
161+
z: number;
162+
constructor(x: number, y: number, z: number);
163+
}
164+
interface Printable {
165+
print(): void;
166+
}
167+
declare const Printable: <T extends Constructor<Base>>(superClass: T) => Constructor<Printable> & {
168+
message: string;
169+
} & T;
170+
interface Tagged {
171+
_tag: string;
172+
}
173+
declare function Tagged<T extends Constructor<{}>>(superClass: T): Constructor<Tagged> & T;
174+
declare const Thing1: Constructor<Tagged> & typeof Derived;
175+
declare const Thing2: Constructor<Tagged> & Constructor<Printable> & {
176+
message: string;
177+
} & typeof Derived;
178+
declare function f1(): void;
179+
declare function f2(): void;
180+
declare class Thing3 extends Thing2 {
181+
constructor(tag: string);
182+
test(): void;
183+
}

0 commit comments

Comments
 (0)