Skip to content

Commit

Permalink
feat: Support for defaulted type parameters
Browse files Browse the repository at this point in the history
Reflect template parameter default values and use them as type arguments (#1348)
  • Loading branch information
krisztianb committed Aug 23, 2020
1 parent 96ede6a commit f67f8db
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 57 deletions.
2 changes: 1 addition & 1 deletion src/lib/converter/context.ts
Expand Up @@ -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<ts.TypeNode>): Reflection {
inherit(baseNode: ts.Node, typeArguments?: ReadonlyArray<ts.TypeNode>): Reflection {
const wasInherit = this.isInherit;
const oldInherited = this.inherited;
const oldInheritParent = this.inheritParent;
Expand Down
3 changes: 3 additions & 0 deletions src/lib/converter/factories/type-parameter.ts
Expand Up @@ -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 = <TypeParameterContainer> context.scope;
const typeParameterReflection = new TypeParameterReflection(typeParameter, reflection);
Expand Down
13 changes: 12 additions & 1 deletion src/lib/converter/nodes/class.ts
Expand Up @@ -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<ts.ClassDeclaration> {
Expand Down Expand Up @@ -64,14 +65,24 @@ export class ClassConverter extends ConverterNodeComponent<ts.ClassDeclaration>
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
// 2. symbol.declarations is non-nullable
// 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);
});
});
}
Expand Down
51 changes: 51 additions & 0 deletions 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<ts.TypeParameterDeclaration> {
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<ts.TypeNode>
): ReadonlyArray<ts.TypeNode> {
if (!typeArguments || typeParams.length > typeArguments.length) {
const typeArgumentsWithDefaults = new Array<ts.TypeNode>();

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;
}
3 changes: 3 additions & 0 deletions src/lib/models/reflections/type-parameter.ts
Expand Up @@ -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;
}
}
30 changes: 25 additions & 5 deletions src/lib/models/types/type-parameter.ts
Expand Up @@ -15,6 +15,15 @@ export class TypeParameterType extends Type {

constraint?: Type;

/**
* Default type for the type parameter.
*
* ```
* class SomeClass<T = {}>
* ```
*/
default?: Type;

/**
* The type name identifier.
*/
Expand All @@ -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;
}

Expand All @@ -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;
}
}
2 changes: 1 addition & 1 deletion src/lib/serialization/schema.ts
Expand Up @@ -222,7 +222,7 @@ export interface TupleType extends Type, S<M.TupleType, 'type'> {
export interface TypeOperatorType extends Type, S<M.TypeOperatorType, 'type' | 'operator' | 'target'> {
}

export interface TypeParameterType extends Type, S<M.TypeParameterType, 'type' | 'name' | 'constraint'> {
export interface TypeParameterType extends Type, S<M.TypeParameterType, 'type' | 'name' | 'constraint' | 'default'> {
}

export interface UnionType extends Type, S<M.UnionType, 'type' | 'types'> {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/serialization/serializers/types/type-parameter.ts
Expand Up @@ -17,6 +17,9 @@ export class TypeParameterTypeSerializer extends TypeSerializerComponent<TypePar
if (type.constraint) {
result.constraint = this.owner.toObject(type.constraint);
}
if (type.default) {
result.default = this.owner.toObject(type.default);
}

return result;
}
Expand Down

0 comments on commit f67f8db

Please sign in to comment.