diff --git a/src/lib/converter/context.ts b/src/lib/converter/context.ts index de9ee081f..70f7917e2 100644 --- a/src/lib/converter/context.ts +++ b/src/lib/converter/context.ts @@ -296,7 +296,7 @@ export class Context { * @param typeArguments The type arguments that apply while inheriting the given node. * @return The resulting reflection / the current scope. */ - inherit(baseNode: ts.Node, typeArguments?: ts.NodeArray): Reflection { + inherit(baseNode: ts.Node, typeArguments?: ReadonlyArray): Reflection { const wasInherit = this.isInherit; const oldInherited = this.inherited; const oldInheritParent = this.inheritParent; diff --git a/src/lib/converter/factories/type-parameter.ts b/src/lib/converter/factories/type-parameter.ts index 82ba5dfd6..e13e756d7 100644 --- a/src/lib/converter/factories/type-parameter.ts +++ b/src/lib/converter/factories/type-parameter.ts @@ -20,6 +20,9 @@ export function createTypeParameter(context: Context, node: ts.TypeParameterDecl if (node.constraint) { typeParameter.constraint = context.converter.convertType(context, node.constraint); } + if (node.default) { + typeParameter.default = context.converter.convertType(context, node.default); + } const reflection = context.scope; const typeParameterReflection = new TypeParameterReflection(typeParameter, reflection); diff --git a/src/lib/converter/nodes/class.ts b/src/lib/converter/nodes/class.ts index f4807e57e..8f9097dc7 100644 --- a/src/lib/converter/nodes/class.ts +++ b/src/lib/converter/nodes/class.ts @@ -6,6 +6,7 @@ import { createDeclaration } from '../factories/index'; import { Context } from '../context'; import { Component, ConverterNodeComponent } from '../components'; import { toArray } from 'lodash'; +import { getTypeArgumentsWithDefaults, getTypeParametersOfType } from '../utils/types'; @Component({name: 'node:class'}) export class ClassConverter extends ConverterNodeComponent { @@ -64,6 +65,16 @@ export class ClassConverter extends ConverterNodeComponent if (type) { const typesToInheritFrom: ts.Type[] = type.isIntersection() ? type.types : [ type ]; + // Get type parameters of all types + let typeParams: ts.TypeParameterDeclaration[] = []; + for (const typeToInheritFrom of typesToInheritFrom) { + typeParams = typeParams.concat(getTypeParametersOfType(typeToInheritFrom)); + } + + const typeArguments = typeParams.length > 0 + ? getTypeArgumentsWithDefaults(typeParams, baseType.typeArguments) + : undefined; + typesToInheritFrom.forEach((typeToInheritFrom: ts.Type) => { // TODO: The TS declaration file claims that: // 1. type.symbol is non-nullable @@ -71,7 +82,7 @@ export class ClassConverter extends ConverterNodeComponent // These are both incorrect, GH#1207 for #2 and existing tests for #1. // Figure out why this is the case and document. typeToInheritFrom.symbol?.declarations?.forEach((declaration) => { - context.inherit(declaration, baseType.typeArguments); + context.inherit(declaration, typeArguments); }); }); } diff --git a/src/lib/converter/utils/types.ts b/src/lib/converter/utils/types.ts new file mode 100644 index 000000000..d88beae81 --- /dev/null +++ b/src/lib/converter/utils/types.ts @@ -0,0 +1,51 @@ +import * as ts from 'typescript'; + +/** + * Returns the type parameters of a given type. + * @param type The type whos type parameters are wanted. + * @returns The type parameters of the type. An empty array if the type has no type parameters. + */ +export function getTypeParametersOfType(type: ts.Type): ReadonlyArray { + const declarations = type.getSymbol()?.getDeclarations() ?? []; + + for (const declaration of declarations) { + if ((ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && + declaration.typeParameters) { + return declaration.typeParameters; + } + } + + return []; +} + +/** + * Returns a list of type arguments. If a type parameter has no corresponding type argument, the default type + * for that type parameter is used as the type argument. + * @param typeParams The type parameters for which the type arguments are wanted. + * @param typeArguments The type arguments as provided in the declaration. + * @returns The complete list of type arguments with possible default values if type arguments are missing. + */ +export function getTypeArgumentsWithDefaults( + typeParams: ts.TypeParameterDeclaration[], + typeArguments?: ReadonlyArray +): ReadonlyArray { + if (!typeArguments || typeParams.length > typeArguments.length) { + const typeArgumentsWithDefaults = new Array(); + + for (let i = 0; i < typeParams.length; ++i) { + if (typeArguments && typeArguments[i]) { + typeArgumentsWithDefaults.push(typeArguments[i]); + } else { + const defaultType = typeParams[i].default; + + if (defaultType) { + typeArgumentsWithDefaults.push(defaultType); + } + } + } + + return typeArgumentsWithDefaults; + } + + return typeArguments; +} diff --git a/src/lib/models/reflections/type-parameter.ts b/src/lib/models/reflections/type-parameter.ts index e27b22340..68f971a17 100644 --- a/src/lib/models/reflections/type-parameter.ts +++ b/src/lib/models/reflections/type-parameter.ts @@ -7,11 +7,14 @@ export class TypeParameterReflection extends Reflection implements TypeContainer type?: Type; + default?: Type; + /** * Create a new TypeParameterReflection instance. */ constructor(type: TypeParameterType, parent?: Reflection) { super(type.name, ReflectionKind.TypeParameter, parent); this.type = type.constraint; + this.default = type.default; } } diff --git a/src/lib/models/types/type-parameter.ts b/src/lib/models/types/type-parameter.ts index efc07f404..966eb424a 100644 --- a/src/lib/models/types/type-parameter.ts +++ b/src/lib/models/types/type-parameter.ts @@ -15,6 +15,15 @@ export class TypeParameterType extends Type { constraint?: Type; + /** + * Default type for the type parameter. + * + * ``` + * class SomeClass + * ``` + */ + default?: Type; + /** * The type name identifier. */ @@ -33,6 +42,7 @@ export class TypeParameterType extends Type { clone(): Type { const clone = new TypeParameterType(this.name); clone.constraint = this.constraint; + clone.default = this.default; return clone; } @@ -47,19 +57,29 @@ export class TypeParameterType extends Type { return false; } + let constraintEquals = false; + if (this.constraint && type.constraint) { - return type.constraint.equals(this.constraint); + constraintEquals = type.constraint.equals(this.constraint); } else if (!this.constraint && !type.constraint) { - return true; - } else { - return false; + constraintEquals = true; } + + let defaultEquals = false; + + if (this.default && type.default) { + defaultEquals = type.default.equals(this.default); + } else if (!this.default && !type.default) { + defaultEquals = true; + } + + return constraintEquals && defaultEquals; } /** * Return a string representation of this type. */ - toString() { + toString(): string { return this.name; } } diff --git a/src/lib/serialization/schema.ts b/src/lib/serialization/schema.ts index c081cdca4..240fae833 100644 --- a/src/lib/serialization/schema.ts +++ b/src/lib/serialization/schema.ts @@ -222,7 +222,7 @@ export interface TupleType extends Type, S { export interface TypeOperatorType extends Type, S { } -export interface TypeParameterType extends Type, S { +export interface TypeParameterType extends Type, S { } export interface UnionType extends Type, S { diff --git a/src/lib/serialization/serializers/types/type-parameter.ts b/src/lib/serialization/serializers/types/type-parameter.ts index ab2925700..094a80736 100644 --- a/src/lib/serialization/serializers/types/type-parameter.ts +++ b/src/lib/serialization/serializers/types/type-parameter.ts @@ -17,6 +17,9 @@ export class TypeParameterTypeSerializer extends TypeSerializerComponent