Skip to content

Allow deriving from object and intersection types #13604

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

Merged
merged 9 commits into from
Jan 21, 2017
120 changes: 71 additions & 49 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3732,11 +3732,16 @@ namespace ts {
return getObjectFlags(type) & ObjectFlags.Reference ? (<TypeReference>type).target : type;
}

function hasBaseType(type: InterfaceType, checkBase: InterfaceType) {
function hasBaseType(type: BaseType, checkBase: BaseType) {
return check(type);
function check(type: InterfaceType): boolean {
const target = <InterfaceType>getTargetType(type);
return target === checkBase || forEach(getBaseTypes(target), check);
function check(type: BaseType): boolean {
if (getObjectFlags(type) & (ObjectFlags.ClassOrInterface | ObjectFlags.Reference)) {
const target = <InterfaceType>getTargetType(type);
return target === checkBase || forEach(getBaseTypes(target), check);
}
else if (type.flags & TypeFlags.Intersection) {
return forEach((<IntersectionType>type).types, check);
}
}
}

Expand Down Expand Up @@ -3805,7 +3810,7 @@ namespace ts {
}

function isConstructorType(type: Type): boolean {
return type.flags & TypeFlags.Object && getSignaturesOfType(type, SignatureKind.Construct).length > 0;
return isValidBaseType(type) && getSignaturesOfType(type, SignatureKind.Construct).length > 0;
}

function getBaseTypeNodeOfClass(type: InterfaceType): ExpressionWithTypeArguments {
Expand Down Expand Up @@ -3844,7 +3849,7 @@ namespace ts {
return unknownType;
}
const baseConstructorType = checkExpression(baseTypeNode.expression);
if (baseConstructorType.flags & TypeFlags.Object) {
if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection)) {
// Resolving the members of a class requires us to resolve the base class of that class.
// We force resolution here such that we catch circularities now.
resolveStructuredTypeMembers(<ObjectType>baseConstructorType);
Expand All @@ -3862,7 +3867,7 @@ namespace ts {
return type.resolvedBaseConstructorType;
}

function getBaseTypes(type: InterfaceType): ObjectType[] {
function getBaseTypes(type: InterfaceType): BaseType[] {
if (!type.resolvedBaseTypes) {
if (type.objectFlags & ObjectFlags.Tuple) {
type.resolvedBaseTypes = [createArrayType(getUnionType(type.typeParameters))];
Expand All @@ -3885,7 +3890,7 @@ namespace ts {
function resolveBaseTypesOfClass(type: InterfaceType): void {
type.resolvedBaseTypes = type.resolvedBaseTypes || emptyArray;
const baseConstructorType = <ObjectType>getBaseConstructorTypeOfClass(type);
if (!(baseConstructorType.flags & TypeFlags.Object)) {
if (!(baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection))) {
return;
}
const baseTypeNode = getBaseTypeNodeOfClass(type);
Expand Down Expand Up @@ -3922,11 +3927,11 @@ namespace ts {
if (baseType === unknownType) {
return;
}
if (!(getObjectFlags(getTargetType(baseType)) & ObjectFlags.ClassOrInterface)) {
if (!isValidBaseType(baseType)) {
error(baseTypeNode.expression, Diagnostics.Base_constructor_return_type_0_is_not_a_class_or_interface_type, typeToString(baseType));
return;
}
if (type === baseType || hasBaseType(<InterfaceType>baseType, type)) {
if (type === baseType || hasBaseType(<BaseType>baseType, type)) {
error(valueDecl, Diagnostics.Type_0_recursively_references_itself_as_a_base_type,
typeToString(type, /*enclosingDeclaration*/ undefined, TypeFormatFlags.WriteArrayAsGenericType));
return;
Expand All @@ -3951,15 +3956,22 @@ namespace ts {
return true;
}

// A valid base type is any non-generic object type or intersection of non-generic
// object types.
function isValidBaseType(type: Type): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return type could be : type is BaseType ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, BaseType is the most restrictive type we can express for a valid base type, but there are still BaseType instances that aren't valid base types (e.g. intersections containing type parameters). So, wouldn't be correct in the negative sense.

return type.flags & TypeFlags.Object && !isGenericMappedType(type) ||
type.flags & TypeFlags.Intersection && !forEach((<IntersectionType>type).types, t => !isValidBaseType(t));
}

function resolveBaseTypesOfInterface(type: InterfaceType): void {
type.resolvedBaseTypes = type.resolvedBaseTypes || emptyArray;
for (const declaration of type.symbol.declarations) {
if (declaration.kind === SyntaxKind.InterfaceDeclaration && getInterfaceBaseTypeNodes(<InterfaceDeclaration>declaration)) {
for (const node of getInterfaceBaseTypeNodes(<InterfaceDeclaration>declaration)) {
const baseType = getTypeFromTypeNode(node);
if (baseType !== unknownType) {
if (getObjectFlags(getTargetType(baseType)) & ObjectFlags.ClassOrInterface) {
if (type !== baseType && !hasBaseType(<InterfaceType>baseType, type)) {
if (isValidBaseType(baseType)) {
if (type !== baseType && !hasBaseType(<BaseType>baseType, type)) {
if (type.resolvedBaseTypes === emptyArray) {
type.resolvedBaseTypes = [<ObjectType>baseType];
}
Expand Down Expand Up @@ -4314,8 +4326,14 @@ namespace ts {

function getTypeWithThisArgument(type: Type, thisArgument?: Type): Type {
if (getObjectFlags(type) & ObjectFlags.Reference) {
return createTypeReference((<TypeReference>type).target,
concatenate((<TypeReference>type).typeArguments, [thisArgument || (<TypeReference>type).target.thisType]));
const target = (<TypeReference>type).target;
const typeArguments = (<TypeReference>type).typeArguments;
if (length(target.typeParameters) === length(typeArguments)) {
return createTypeReference(target, concatenate(typeArguments, [thisArgument || target.thisType]));
}
}
else if (type.flags & TypeFlags.Intersection) {
return getIntersectionType(map((<IntersectionType>type).types, t => getTypeWithThisArgument(t, thisArgument)));
}
return type;
}
Expand Down Expand Up @@ -4350,8 +4368,8 @@ namespace ts {
}
const thisArgument = lastOrUndefined(typeArguments);
for (const baseType of baseTypes) {
const instantiatedBaseType = thisArgument ? getTypeWithThisArgument(<ObjectType>instantiateType(baseType, mapper), thisArgument) : baseType;
addInheritedMembers(members, getPropertiesOfObjectType(instantiatedBaseType));
const instantiatedBaseType = thisArgument ? getTypeWithThisArgument(instantiateType(baseType, mapper), thisArgument) : baseType;
addInheritedMembers(members, getPropertiesOfType(instantiatedBaseType));
callSignatures = concatenate(callSignatures, getSignaturesOfType(instantiatedBaseType, SignatureKind.Call));
constructSignatures = concatenate(constructSignatures, getSignaturesOfType(instantiatedBaseType, SignatureKind.Construct));
stringIndexInfo = stringIndexInfo || getIndexInfoOfType(instantiatedBaseType, IndexKind.String);
Expand Down Expand Up @@ -4573,9 +4591,9 @@ namespace ts {
constructSignatures = getDefaultConstructSignatures(classType);
}
const baseConstructorType = getBaseConstructorTypeOfClass(classType);
if (baseConstructorType.flags & TypeFlags.Object) {
if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection)) {
members = createSymbolTable(getNamedMembers(members));
addInheritedMembers(members, getPropertiesOfObjectType(baseConstructorType));
addInheritedMembers(members, getPropertiesOfType(baseConstructorType));
}
}
const numberIndexInfo = symbol.flags & SymbolFlags.Enum ? enumNumberIndexInfo : undefined;
Expand Down Expand Up @@ -4739,28 +4757,26 @@ namespace ts {
}

function getPropertiesOfUnionOrIntersectionType(type: UnionOrIntersectionType): Symbol[] {
for (const current of type.types) {
for (const prop of getPropertiesOfType(current)) {
getUnionOrIntersectionProperty(type, prop.name);
}
// The properties of a union type are those that are present in all constituent types, so
// we only need to check the properties of the first type
if (type.flags & TypeFlags.Union) {
break;
}
}
const props = type.resolvedProperties;
if (props) {
const result: Symbol[] = [];
props.forEach(prop => {
// We need to filter out partial properties in union types
if (!(prop.flags & SymbolFlags.SyntheticProperty && (<TransientSymbol>prop).isPartial)) {
result.push(prop);
if (!type.resolvedProperties) {
const members = createMap<Symbol>();
for (const current of type.types) {
for (const prop of getPropertiesOfType(current)) {
if (!members.has(prop.name)) {
const combinedProp = getPropertyOfUnionOrIntersectionType(type, prop.name);
if (combinedProp) {
members.set(prop.name, combinedProp);
}
}
}
});
return result;
// The properties of a union type are those that are present in all constituent types, so
// we only need to check the properties of the first type
if (type.flags & TypeFlags.Union) {
break;
}
}
type.resolvedProperties = getNamedMembers(members);
}
return emptyArray;
return type.resolvedProperties;
}

function getPropertiesOfType(type: Type): Symbol[] {
Expand Down Expand Up @@ -4845,14 +4861,19 @@ namespace ts {
}
}

function getApparentTypeOfIntersectionType(type: IntersectionType) {
return type.resolvedIndexType || (type.resolvedApparentType = getTypeWithThisArgument(type, type));
}

/**
* For a type parameter, return the base constraint of the type parameter. For the string, number,
* boolean, and symbol primitive types, return the corresponding object types. Otherwise return the
* type itself. Note that the apparent type of a union type is the union type itself.
*/
function getApparentType(type: Type): Type {
const t = type.flags & TypeFlags.TypeVariable ? getBaseConstraintOfType(<TypeVariable>type) || emptyObjectType : type;
return t.flags & TypeFlags.StringLike ? globalStringType :
return t.flags & TypeFlags.Intersection ? getApparentTypeOfIntersectionType(<IntersectionType>type) :
t.flags & TypeFlags.StringLike ? globalStringType :
t.flags & TypeFlags.NumberLike ? globalNumberType :
t.flags & TypeFlags.BooleanLike ? globalBooleanType :
t.flags & TypeFlags.ESSymbol ? getGlobalESSymbolType() :
Expand Down Expand Up @@ -4928,7 +4949,7 @@ namespace ts {
// these partial properties when identifying discriminant properties, but otherwise they are filtered out
// and do not appear to be present in the union type.
function getUnionOrIntersectionProperty(type: UnionOrIntersectionType, name: string): Symbol {
const properties = type.resolvedProperties || (type.resolvedProperties = createMap<Symbol>());
const properties = type.propertyCache || (type.propertyCache = createMap<Symbol>());
let property = properties.get(name);
if (!property) {
property = createUnionOrIntersectionProperty(type, name);
Expand Down Expand Up @@ -18216,16 +18237,18 @@ namespace ts {
return;
}

const propDeclaration = prop.valueDeclaration;

// index is numeric and property name is not valid numeric literal
if (indexKind === IndexKind.Number && !isNumericName(prop.valueDeclaration.name)) {
if (indexKind === IndexKind.Number && propDeclaration && !isNumericName(propDeclaration.name)) {
return;
}

// perform property check if property or indexer is declared in 'type'
// this allows to rule out cases when both property and indexer are inherited from the base class
let errorNode: Node;
if (prop.valueDeclaration.name.kind === SyntaxKind.ComputedPropertyName || prop.parent === containingType.symbol) {
errorNode = prop.valueDeclaration;
if (propDeclaration && propDeclaration.name.kind === SyntaxKind.ComputedPropertyName || prop.parent === containingType.symbol) {
errorNode = propDeclaration;
}
else if (indexDeclaration) {
errorNode = indexDeclaration;
Expand Down Expand Up @@ -18364,7 +18387,7 @@ namespace ts {
checkTypeAssignableTo(staticType, getTypeWithoutSignatures(staticBaseType), node.name || node,
Diagnostics.Class_static_side_0_incorrectly_extends_base_class_static_side_1);

if (baseType.symbol.valueDeclaration &&
if (baseType.symbol && baseType.symbol.valueDeclaration &&
!isInAmbientContext(baseType.symbol.valueDeclaration) &&
baseType.symbol.valueDeclaration.kind === SyntaxKind.ClassDeclaration) {
if (!isBlockScopedNameDeclaredBeforeUse(baseType.symbol.valueDeclaration, node)) {
Expand Down Expand Up @@ -18396,8 +18419,7 @@ namespace ts {
if (produceDiagnostics) {
const t = getTypeFromTypeNode(typeRefNode);
if (t !== unknownType) {
const declaredType = getObjectFlags(t) & ObjectFlags.Reference ? (<TypeReference>t).target : t;
if (getObjectFlags(declaredType) & ObjectFlags.ClassOrInterface) {
if (isValidBaseType(t)) {
checkTypeAssignableTo(typeWithThis, getTypeWithThisArgument(t, type.thisType), node.name || node, Diagnostics.Class_0_incorrectly_implements_interface_1);
}
else {
Expand Down Expand Up @@ -18437,7 +18459,7 @@ namespace ts {
return forEach(symbol.declarations, d => isClassLike(d) ? d : undefined);
}

function checkKindsOfPropertyMemberOverrides(type: InterfaceType, baseType: ObjectType): void {
function checkKindsOfPropertyMemberOverrides(type: InterfaceType, baseType: BaseType): void {

// TypeScript 1.0 spec (April 2014): 8.2.3
// A derived class inherits all members from its base class it doesn't override.
Expand All @@ -18454,7 +18476,7 @@ namespace ts {
// derived class instance member variables and accessors, but not by other kinds of members.

// NOTE: assignability is checked in checkClassDeclaration
const baseProperties = getPropertiesOfObjectType(baseType);
const baseProperties = getPropertiesOfType(baseType);
for (const baseProperty of baseProperties) {
const base = getTargetSymbol(baseProperty);

Expand Down Expand Up @@ -18578,7 +18600,7 @@ namespace ts {
let ok = true;

for (const base of baseTypes) {
const properties = getPropertiesOfObjectType(getTypeWithThisArgument(base, type.thisType));
const properties = getPropertiesOfType(getTypeWithThisArgument(base, type.thisType));
for (const prop of properties) {
const existing = seen.get(prop.name);
if (!existing) {
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ namespace ts {
GreaterThan = 1
}

export function length(array: any[]) {
return array ? array.length : 0;
}

/**
* Iterates through 'array' by index and performs the callback on each element of array until the callback
* returns a truthy value, then returns that value.
Expand Down
16 changes: 12 additions & 4 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2358,7 +2358,7 @@
getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo;
getSignaturesOfType(type: Type, kind: SignatureKind): Signature[];
getIndexTypeOfType(type: Type, kind: IndexKind): Type;
getBaseTypes(type: InterfaceType): ObjectType[];
getBaseTypes(type: InterfaceType): BaseType[];
getReturnTypeOfSignature(signature: Signature): Type;
/**
* Gets the type of a parameter at a given position in a signature.
Expand Down Expand Up @@ -2910,9 +2910,12 @@
/* @internal */
resolvedBaseConstructorType?: Type; // Resolved base constructor type of class
/* @internal */
resolvedBaseTypes: ObjectType[]; // Resolved base types
resolvedBaseTypes: BaseType[]; // Resolved base types
}

// Object type or intersection of object types
export type BaseType = ObjectType | IntersectionType;

export interface InterfaceTypeWithDeclaredMembers extends InterfaceType {
declaredProperties: Symbol[]; // Declared members
declaredCallSignatures: Signature[]; // Declared call signatures
Expand Down Expand Up @@ -2945,7 +2948,9 @@
export interface UnionOrIntersectionType extends Type {
types: Type[]; // Constituent types
/* @internal */
resolvedProperties: SymbolTable; // Cache of resolved properties
propertyCache: SymbolTable; // Cache of resolved properties
/* @internal */
resolvedProperties: Symbol[];
/* @internal */
resolvedIndexType: IndexType;
/* @internal */
Expand All @@ -2956,7 +2961,10 @@

export interface UnionType extends UnionOrIntersectionType { }

export interface IntersectionType extends UnionOrIntersectionType { }
export interface IntersectionType extends UnionOrIntersectionType {
/* @internal */
resolvedApparentType: Type;
}

export type StructuredType = ObjectType | UnionType | IntersectionType;

Expand Down
2 changes: 1 addition & 1 deletion src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ namespace ts {
getNumberIndexType(): Type {
return this.checker.getIndexTypeOfType(this, IndexKind.Number);
}
getBaseTypes(): ObjectType[] {
getBaseTypes(): BaseType[] {
return this.flags & TypeFlags.Object && this.objectFlags & (ObjectFlags.Class | ObjectFlags.Interface)
? this.checker.getBaseTypes(<InterfaceType><Type>this)
: undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ namespace ts {
getConstructSignatures(): Signature[];
getStringIndexType(): Type;
getNumberIndexType(): Type;
getBaseTypes(): ObjectType[];
getBaseTypes(): BaseType[];
getNonNullableType(): Type;
}

Expand Down
Loading