Skip to content

Commit

Permalink
Merge pull request #13743 from Microsoft/mixinClasses
Browse files Browse the repository at this point in the history
Mixin classes
  • Loading branch information
ahejlsberg authored Jan 30, 2017
2 parents 9b6f930 + 004dbf4 commit f32f95a
Show file tree
Hide file tree
Showing 14 changed files with 2,298 additions and 14 deletions.
96 changes: 82 additions & 14 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2465,7 +2465,8 @@ namespace ts {
const symbol = type.symbol;
if (symbol) {
// Always use 'typeof T' for type of class, enum, and module objects
if (symbol.flags & (SymbolFlags.Class | SymbolFlags.Enum | SymbolFlags.ValueModule)) {
if (symbol.flags & SymbolFlags.Class && !getBaseTypeVariableOfClass(symbol) ||
symbol.flags & (SymbolFlags.Enum | SymbolFlags.ValueModule)) {
writeTypeOfSymbol(type, flags);
}
else if (shouldWriteTypeOfFunctionSymbol()) {
Expand Down Expand Up @@ -3639,6 +3640,11 @@ namespace ts {
return links.type;
}

function getBaseTypeVariableOfClass(symbol: Symbol) {
const baseConstructorType = getBaseConstructorTypeOfClass(getDeclaredTypeOfClassOrInterface(symbol));
return baseConstructorType.flags & TypeFlags.TypeVariable ? baseConstructorType : undefined;
}

function getTypeOfFuncClassEnumModule(symbol: Symbol): Type {
const links = getSymbolLinks(symbol);
if (!links.type) {
Expand All @@ -3647,8 +3653,13 @@ namespace ts {
}
else {
const type = createObjectType(ObjectFlags.Anonymous, symbol);
links.type = strictNullChecks && symbol.flags & SymbolFlags.Optional ?
includeFalsyTypes(type, TypeFlags.Undefined) : type;
if (symbol.flags & SymbolFlags.Class) {
const baseTypeVariable = getBaseTypeVariableOfClass(symbol);
links.type = baseTypeVariable ? getIntersectionType([type, baseTypeVariable]) : type;
}
else {
links.type = strictNullChecks && symbol.flags & SymbolFlags.Optional ? includeFalsyTypes(type, TypeFlags.Undefined) : type;
}
}
}
return links.type;
Expand Down Expand Up @@ -3812,8 +3823,26 @@ 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[].
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;
}
return false;
}

function isConstructorType(type: Type): boolean {
return isValidBaseType(type) && getSignaturesOfType(type, SignatureKind.Construct).length > 0;
if (isValidBaseType(type) && getSignaturesOfType(type, SignatureKind.Construct).length > 0) {
return true;
}
if (type.flags & TypeFlags.TypeVariable) {
const constraint = getBaseConstraintOfType(<TypeVariable>type);
return isValidBaseType(constraint) && isMixinConstructorType(constraint);
}
return false;
}

function getBaseTypeNodeOfClass(type: InterfaceType): ExpressionWithTypeArguments {
Expand Down Expand Up @@ -3892,7 +3921,7 @@ namespace ts {

function resolveBaseTypesOfClass(type: InterfaceType): void {
type.resolvedBaseTypes = type.resolvedBaseTypes || emptyArray;
const baseConstructorType = <ObjectType>getBaseConstructorTypeOfClass(type);
const baseConstructorType = getApparentType(getBaseConstructorTypeOfClass(type));
if (!(baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection))) {
return;
}
Expand Down Expand Up @@ -4542,16 +4571,47 @@ namespace ts {
getUnionType([info1.type, info2.type]), info1.isReadonly || info2.isReadonly);
}

function includeMixinType(type: Type, types: 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).
let callSignatures: Signature[] = emptyArray;
let constructSignatures: Signature[] = emptyArray;
let stringIndexInfo: IndexInfo = undefined;
let numberIndexInfo: IndexInfo = undefined;
for (const t of type.types) {
let stringIndexInfo: IndexInfo;
let numberIndexInfo: IndexInfo;
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);
}
callSignatures = concatenate(callSignatures, getSignaturesOfType(t, SignatureKind.Call));
constructSignatures = concatenate(constructSignatures, getSignaturesOfType(t, SignatureKind.Construct));
stringIndexInfo = intersectIndexInfos(stringIndexInfo, getIndexInfoOfType(t, IndexKind.String));
numberIndexInfo = intersectIndexInfos(numberIndexInfo, getIndexInfoOfType(t, IndexKind.Number));
}
Expand Down Expand Up @@ -4593,7 +4653,7 @@ namespace ts {
constructSignatures = getDefaultConstructSignatures(classType);
}
const baseConstructorType = getBaseConstructorTypeOfClass(classType);
if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection)) {
if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection | TypeFlags.TypeVariable)) {
members = createSymbolTable(getNamedMembers(members));
addInheritedMembers(members, getPropertiesOfType(baseConstructorType));
}
Expand Down Expand Up @@ -4890,6 +4950,7 @@ namespace ts {

function createUnionOrIntersectionProperty(containingType: UnionOrIntersectionType, name: string): Symbol {
const types = containingType.types;
const excludeModifiers = containingType.flags & TypeFlags.Union ? ModifierFlags.Private | ModifierFlags.Protected : 0;
let props: Symbol[];
// Flags we want to propagate to the result if they exist in all source symbols
let commonFlags = (containingType.flags & TypeFlags.Intersection) ? SymbolFlags.Optional : SymbolFlags.None;
Expand All @@ -4899,7 +4960,7 @@ namespace ts {
const type = getApparentType(current);
if (type !== unknownType) {
const prop = getPropertyOfType(type, name);
if (prop && !(getDeclarationModifierFlagsFromSymbol(prop) & (ModifierFlags.Private | ModifierFlags.Protected))) {
if (prop && !(getDeclarationModifierFlagsFromSymbol(prop) & excludeModifiers)) {
commonFlags &= prop.flags;
if (!props) {
props = [prop];
Expand Down Expand Up @@ -6965,9 +7026,12 @@ namespace ts {
result.properties = resolved.properties;
result.callSignatures = emptyArray;
result.constructSignatures = emptyArray;
type = result;
return result;
}
}
else if (type.flags & TypeFlags.Intersection) {
return getIntersectionType(map((<IntersectionType>type).types, getTypeWithoutSignatures));
}
return type;
}

Expand Down Expand Up @@ -18390,7 +18454,8 @@ namespace ts {
const baseTypes = getBaseTypes(type);
if (baseTypes.length && produceDiagnostics) {
const baseType = baseTypes[0];
const staticBaseType = getBaseConstructorTypeOfClass(type);
const baseConstructorType = getBaseConstructorTypeOfClass(type);
const staticBaseType = getApparentType(baseConstructorType);
checkBaseTypeAccessibility(staticBaseType, baseTypeNode);
checkSourceElement(baseTypeNode.expression);
if (baseTypeNode.typeArguments) {
Expand All @@ -18404,6 +18469,9 @@ namespace ts {
checkTypeAssignableTo(typeWithThis, getTypeWithThisArgument(baseType, type.thisType), node.name || node, Diagnostics.Class_0_incorrectly_extends_base_class_1);
checkTypeAssignableTo(staticType, getTypeWithoutSignatures(staticBaseType), node.name || node,
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);
}

if (baseType.symbol && baseType.symbol.valueDeclaration &&
!isInAmbientContext(baseType.symbol.valueDeclaration) &&
Expand All @@ -18413,7 +18481,7 @@ namespace ts {
}
}

if (!(staticBaseType.symbol && staticBaseType.symbol.flags & SymbolFlags.Class)) {
if (!(staticBaseType.symbol && staticBaseType.symbol.flags & SymbolFlags.Class) && !(baseConstructorType.flags & TypeFlags.TypeVariable)) {
// When the static base type is a "class-like" constructor function (but not actually a class), we verify
// 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
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,10 @@
"category": "Error",
"code": 2544
},
"A mixin class must have a constructor with a single rest parameter of type 'any[]'.": {
"category": "Error",
"code": 2545
},
"JSX element attributes type '{0}' may not be a union type.": {
"category": "Error",
"code": 2600
Expand Down
183 changes: 183 additions & 0 deletions tests/baselines/reference/mixinClassesAnnotated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
//// [mixinClassesAnnotated.ts]

type Constructor<T> = new(...args: any[]) => T;

class Base {
constructor(public x: number, public y: number) {}
}

class Derived extends Base {
constructor(x: number, y: number, public z: number) {
super(x, y);
}
}

interface Printable {
print(): void;
}

const Printable = <T extends Constructor<Base>>(superClass: T): Constructor<Printable> & { message: string } & T =>
class extends superClass {
static message = "hello";
print() {
const output = this.x + "," + this.y;
}
}

interface Tagged {
_tag: string;
}

function Tagged<T extends Constructor<{}>>(superClass: T): Constructor<Tagged> & T {
class C extends superClass {
_tag: string;
constructor(...args: any[]) {
super(...args);
this._tag = "hello";
}
}
return C;
}

const Thing1 = Tagged(Derived);
const Thing2 = Tagged(Printable(Derived));
Thing2.message;

function f1() {
const thing = new Thing1(1, 2, 3);
thing.x;
thing._tag;
}

function f2() {
const thing = new Thing2(1, 2, 3);
thing.x;
thing._tag;
thing.print();
}

class Thing3 extends Thing2 {
constructor(tag: string) {
super(10, 20, 30);
this._tag = tag;
}
test() {
this.print();
}
}


//// [mixinClassesAnnotated.js]
var __extends = (this && this.__extends) || (function () {
var 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 function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Base = (function () {
function Base(x, y) {
this.x = x;
this.y = y;
}
return Base;
}());
var Derived = (function (_super) {
__extends(Derived, _super);
function Derived(x, y, z) {
var _this = _super.call(this, x, y) || this;
_this.z = z;
return _this;
}
return Derived;
}(Base));
var Printable = function (superClass) { return _a = (function (_super) {
__extends(class_1, _super);
function class_1() {
return _super !== null && _super.apply(this, arguments) || this;
}
class_1.prototype.print = function () {
var output = this.x + "," + this.y;
};
return class_1;
}(superClass)),
_a.message = "hello",
_a; var _a; };
function Tagged(superClass) {
var C = (function (_super) {
__extends(C, _super);
function C() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var _this = _super.apply(this, args) || this;
_this._tag = "hello";
return _this;
}
return C;
}(superClass));
return C;
}
var Thing1 = Tagged(Derived);
var Thing2 = Tagged(Printable(Derived));
Thing2.message;
function f1() {
var thing = new Thing1(1, 2, 3);
thing.x;
thing._tag;
}
function f2() {
var thing = new Thing2(1, 2, 3);
thing.x;
thing._tag;
thing.print();
}
var Thing3 = (function (_super) {
__extends(Thing3, _super);
function Thing3(tag) {
var _this = _super.call(this, 10, 20, 30) || this;
_this._tag = tag;
return _this;
}
Thing3.prototype.test = function () {
this.print();
};
return Thing3;
}(Thing2));


//// [mixinClassesAnnotated.d.ts]
declare type Constructor<T> = new (...args: any[]) => T;
declare class Base {
x: number;
y: number;
constructor(x: number, y: number);
}
declare class Derived extends Base {
z: number;
constructor(x: number, y: number, z: number);
}
interface Printable {
print(): void;
}
declare const Printable: <T extends Constructor<Base>>(superClass: T) => Constructor<Printable> & {
message: string;
} & T;
interface Tagged {
_tag: string;
}
declare function Tagged<T extends Constructor<{}>>(superClass: T): Constructor<Tagged> & T;
declare const Thing1: Constructor<Tagged> & typeof Derived;
declare const Thing2: Constructor<Tagged> & Constructor<Printable> & {
message: string;
} & typeof Derived;
declare function f1(): void;
declare function f2(): void;
declare class Thing3 extends Thing2 {
constructor(tag: string);
test(): void;
}
Loading

0 comments on commit f32f95a

Please sign in to comment.