Skip to content

Commit f67f8db

Browse files
authored
feat: Support for defaulted type parameters
Reflect template parameter default values and use them as type arguments (#1348)
1 parent 96ede6a commit f67f8db

File tree

10 files changed

+164
-57
lines changed

10 files changed

+164
-57
lines changed

src/lib/converter/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ export class Context {
296296
* @param typeArguments The type arguments that apply while inheriting the given node.
297297
* @return The resulting reflection / the current scope.
298298
*/
299-
inherit(baseNode: ts.Node, typeArguments?: ts.NodeArray<ts.TypeNode>): Reflection {
299+
inherit(baseNode: ts.Node, typeArguments?: ReadonlyArray<ts.TypeNode>): Reflection {
300300
const wasInherit = this.isInherit;
301301
const oldInherited = this.inherited;
302302
const oldInheritParent = this.inheritParent;

src/lib/converter/factories/type-parameter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export function createTypeParameter(context: Context, node: ts.TypeParameterDecl
2020
if (node.constraint) {
2121
typeParameter.constraint = context.converter.convertType(context, node.constraint);
2222
}
23+
if (node.default) {
24+
typeParameter.default = context.converter.convertType(context, node.default);
25+
}
2326

2427
const reflection = <TypeParameterContainer> context.scope;
2528
const typeParameterReflection = new TypeParameterReflection(typeParameter, reflection);

src/lib/converter/nodes/class.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createDeclaration } from '../factories/index';
66
import { Context } from '../context';
77
import { Component, ConverterNodeComponent } from '../components';
88
import { toArray } from 'lodash';
9+
import { getTypeArgumentsWithDefaults, getTypeParametersOfType } from '../utils/types';
910

1011
@Component({name: 'node:class'})
1112
export class ClassConverter extends ConverterNodeComponent<ts.ClassDeclaration> {
@@ -64,14 +65,24 @@ export class ClassConverter extends ConverterNodeComponent<ts.ClassDeclaration>
6465
if (type) {
6566
const typesToInheritFrom: ts.Type[] = type.isIntersection() ? type.types : [ type ];
6667

68+
// Get type parameters of all types
69+
let typeParams: ts.TypeParameterDeclaration[] = [];
70+
for (const typeToInheritFrom of typesToInheritFrom) {
71+
typeParams = typeParams.concat(getTypeParametersOfType(typeToInheritFrom));
72+
}
73+
74+
const typeArguments = typeParams.length > 0
75+
? getTypeArgumentsWithDefaults(typeParams, baseType.typeArguments)
76+
: undefined;
77+
6778
typesToInheritFrom.forEach((typeToInheritFrom: ts.Type) => {
6879
// TODO: The TS declaration file claims that:
6980
// 1. type.symbol is non-nullable
7081
// 2. symbol.declarations is non-nullable
7182
// These are both incorrect, GH#1207 for #2 and existing tests for #1.
7283
// Figure out why this is the case and document.
7384
typeToInheritFrom.symbol?.declarations?.forEach((declaration) => {
74-
context.inherit(declaration, baseType.typeArguments);
85+
context.inherit(declaration, typeArguments);
7586
});
7687
});
7788
}

src/lib/converter/utils/types.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as ts from 'typescript';
2+
3+
/**
4+
* Returns the type parameters of a given type.
5+
* @param type The type whos type parameters are wanted.
6+
* @returns The type parameters of the type. An empty array if the type has no type parameters.
7+
*/
8+
export function getTypeParametersOfType(type: ts.Type): ReadonlyArray<ts.TypeParameterDeclaration> {
9+
const declarations = type.getSymbol()?.getDeclarations() ?? [];
10+
11+
for (const declaration of declarations) {
12+
if ((ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) &&
13+
declaration.typeParameters) {
14+
return declaration.typeParameters;
15+
}
16+
}
17+
18+
return [];
19+
}
20+
21+
/**
22+
* Returns a list of type arguments. If a type parameter has no corresponding type argument, the default type
23+
* for that type parameter is used as the type argument.
24+
* @param typeParams The type parameters for which the type arguments are wanted.
25+
* @param typeArguments The type arguments as provided in the declaration.
26+
* @returns The complete list of type arguments with possible default values if type arguments are missing.
27+
*/
28+
export function getTypeArgumentsWithDefaults(
29+
typeParams: ts.TypeParameterDeclaration[],
30+
typeArguments?: ReadonlyArray<ts.TypeNode>
31+
): ReadonlyArray<ts.TypeNode> {
32+
if (!typeArguments || typeParams.length > typeArguments.length) {
33+
const typeArgumentsWithDefaults = new Array<ts.TypeNode>();
34+
35+
for (let i = 0; i < typeParams.length; ++i) {
36+
if (typeArguments && typeArguments[i]) {
37+
typeArgumentsWithDefaults.push(typeArguments[i]);
38+
} else {
39+
const defaultType = typeParams[i].default;
40+
41+
if (defaultType) {
42+
typeArgumentsWithDefaults.push(defaultType);
43+
}
44+
}
45+
}
46+
47+
return typeArgumentsWithDefaults;
48+
}
49+
50+
return typeArguments;
51+
}

src/lib/models/reflections/type-parameter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ export class TypeParameterReflection extends Reflection implements TypeContainer
77

88
type?: Type;
99

10+
default?: Type;
11+
1012
/**
1113
* Create a new TypeParameterReflection instance.
1214
*/
1315
constructor(type: TypeParameterType, parent?: Reflection) {
1416
super(type.name, ReflectionKind.TypeParameter, parent);
1517
this.type = type.constraint;
18+
this.default = type.default;
1619
}
1720
}

src/lib/models/types/type-parameter.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ export class TypeParameterType extends Type {
1515

1616
constraint?: Type;
1717

18+
/**
19+
* Default type for the type parameter.
20+
*
21+
* ```
22+
* class SomeClass<T = {}>
23+
* ```
24+
*/
25+
default?: Type;
26+
1827
/**
1928
* The type name identifier.
2029
*/
@@ -33,6 +42,7 @@ export class TypeParameterType extends Type {
3342
clone(): Type {
3443
const clone = new TypeParameterType(this.name);
3544
clone.constraint = this.constraint;
45+
clone.default = this.default;
3646
return clone;
3747
}
3848

@@ -47,19 +57,29 @@ export class TypeParameterType extends Type {
4757
return false;
4858
}
4959

60+
let constraintEquals = false;
61+
5062
if (this.constraint && type.constraint) {
51-
return type.constraint.equals(this.constraint);
63+
constraintEquals = type.constraint.equals(this.constraint);
5264
} else if (!this.constraint && !type.constraint) {
53-
return true;
54-
} else {
55-
return false;
65+
constraintEquals = true;
5666
}
67+
68+
let defaultEquals = false;
69+
70+
if (this.default && type.default) {
71+
defaultEquals = type.default.equals(this.default);
72+
} else if (!this.default && !type.default) {
73+
defaultEquals = true;
74+
}
75+
76+
return constraintEquals && defaultEquals;
5777
}
5878

5979
/**
6080
* Return a string representation of this type.
6181
*/
62-
toString() {
82+
toString(): string {
6383
return this.name;
6484
}
6585
}

src/lib/serialization/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export interface TupleType extends Type, S<M.TupleType, 'type'> {
222222
export interface TypeOperatorType extends Type, S<M.TypeOperatorType, 'type' | 'operator' | 'target'> {
223223
}
224224

225-
export interface TypeParameterType extends Type, S<M.TypeParameterType, 'type' | 'name' | 'constraint'> {
225+
export interface TypeParameterType extends Type, S<M.TypeParameterType, 'type' | 'name' | 'constraint' | 'default'> {
226226
}
227227

228228
export interface UnionType extends Type, S<M.UnionType, 'type' | 'types'> {

src/lib/serialization/serializers/types/type-parameter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export class TypeParameterTypeSerializer extends TypeSerializerComponent<TypePar
1717
if (type.constraint) {
1818
result.constraint = this.owner.toObject(type.constraint);
1919
}
20+
if (type.default) {
21+
result.default = this.owner.toObject(type.default);
22+
}
2023

2124
return result;
2225
}

0 commit comments

Comments
 (0)