Skip to content

Commit

Permalink
Propagate type arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
WoH committed Sep 4, 2019
1 parent 28e599d commit 0b7d18c
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 49 deletions.
81 changes: 33 additions & 48 deletions src/metadataGeneration/typeResolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as indexOf from 'lodash.indexof';
import * as map from 'lodash.map';
import * as ts from 'typescript';
import { getJSDocComment, getJSDocTagNames, isExistJSDocTag } from './../utils/jsDocUtils';
import { getPropertyValidators } from './../utils/validatorUtils';
Expand All @@ -20,7 +18,13 @@ const inProgressTypes: { [typeName: string]: boolean } = {};
type UsableDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration | ts.PropertySignature;

export class TypeResolver {
constructor(private readonly typeNode: ts.TypeNode, private readonly current: MetadataGenerator, private readonly parentNode?: ts.Node, private readonly extractEnum = true) {}
constructor(
private readonly typeNode: ts.TypeNode,
private readonly current: MetadataGenerator,
private readonly parentNode?: ts.Node,
private readonly extractEnum = true,
private context: { [name: string]: ts.TypeReferenceNode | ts.TypeNode } = {},
) {}

public static clearCache() {
Object.keys(localReferenceTypeCache).forEach(key => {
Expand All @@ -41,7 +45,7 @@ export class TypeResolver {
if (this.typeNode.kind === ts.SyntaxKind.ArrayType) {
return {
dataType: 'array',
elementType: new TypeResolver((this.typeNode as ts.ArrayTypeNode).elementType, this.current).resolve(),
elementType: new TypeResolver((this.typeNode as ts.ArrayTypeNode).elementType, this.current, this.parentNode, this.extractEnum, this.context).resolve(),
} as Tsoa.ArrayType;
}

Expand All @@ -64,7 +68,7 @@ export class TypeResolver {
} as Tsoa.EnumerateType;
} else {
const types = this.typeNode.types.map(type => {
return new TypeResolver(type, this.current, this.parentNode, this.extractEnum).resolve();
return new TypeResolver(type, this.current, this.parentNode, this.extractEnum, this.context).resolve();
});

return {
Expand All @@ -76,7 +80,7 @@ export class TypeResolver {

if (ts.isIntersectionTypeNode(this.typeNode)) {
const types = this.typeNode.types.map(type => {
return new TypeResolver(type, this.current, this.parentNode, this.extractEnum).resolve();
return new TypeResolver(type, this.current, this.parentNode, this.extractEnum, this.context).resolve();
});

return {
Expand All @@ -93,7 +97,7 @@ export class TypeResolver {
const properties = this.typeNode.members
.filter(member => ts.isPropertySignature(member))
.reduce((res, propertySignature: ts.PropertySignature) => {
const type = new TypeResolver(propertySignature.type as ts.TypeNode, this.current, propertySignature).resolve();
const type = new TypeResolver(propertySignature.type as ts.TypeNode, this.current, propertySignature, this.extractEnum, this.context).resolve();
const property: Tsoa.Property = {
default: getJSDocComment(propertySignature, 'default'),
description: this.getNodeDescription(propertySignature),
Expand All @@ -112,12 +116,12 @@ export class TypeResolver {

if (indexMember) {
const indexSignatureDeclaration = indexMember as ts.IndexSignatureDeclaration;
const indexType = new TypeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current).resolve();
const indexType = new TypeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current, this.parentNode, this.extractEnum, this.context).resolve();
if (indexType.dataType !== 'string') {
throw new GenerateMetadataError(`Only string indexers are supported.`);
}

additionalType = new TypeResolver(indexSignatureDeclaration.type as ts.TypeNode, this.current).resolve();
additionalType = new TypeResolver(indexSignatureDeclaration.type as ts.TypeNode, this.current, this.parentNode, this.extractEnum, this.context).resolve();
}

const objLiteral: Tsoa.ObjectLiteralType = {
Expand Down Expand Up @@ -149,17 +153,21 @@ export class TypeResolver {
if (typeReference.typeName.text === 'Array' && typeReference.typeArguments && typeReference.typeArguments.length === 1) {
return {
dataType: 'array',
elementType: new TypeResolver(typeReference.typeArguments[0], this.current).resolve(),
elementType: new TypeResolver(typeReference.typeArguments[0], this.current, this.parentNode, this.extractEnum, this.context).resolve(),
} as Tsoa.ArrayType;
}

if (typeReference.typeName.text === 'Promise' && typeReference.typeArguments && typeReference.typeArguments.length === 1) {
return new TypeResolver(typeReference.typeArguments[0], this.current).resolve();
return new TypeResolver(typeReference.typeArguments[0], this.current, this.parentNode, this.extractEnum, this.context).resolve();
}

if (typeReference.typeName.text === 'String') {
return { dataType: 'string' } as Tsoa.Type;
}

if (this.context[typeReference.typeName.text]) {
return new TypeResolver(this.context[typeReference.typeName.text], this.current, this.parentNode, this.extractEnum, this.context).resolve();
}
}

if (!this.extractEnum) {
Expand All @@ -175,13 +183,18 @@ export class TypeResolver {
}

let referenceType: Tsoa.ReferenceType;
if (typeReference.typeArguments && typeReference.typeArguments.length === 1) {
const typeT: ts.NodeArray<ts.TypeNode> = typeReference.typeArguments as ts.NodeArray<ts.TypeNode>;
referenceType = this.getReferenceType(typeReference.typeName as ts.EntityName, this.extractEnum, typeT);
} else {
referenceType = this.getReferenceType(typeReference.typeName as ts.EntityName, this.extractEnum);
if (typeReference.typeArguments && typeReference.typeArguments.length > 0) {
const typeParameters = this.getModelTypeDeclaration(typeReference.typeName).typeParameters;
if (typeParameters) {
for (let index = 0; index < typeParameters.length; index++) {
const typeParameter = typeParameters[index];
this.context = { [typeParameter.name.text]: typeReference.typeArguments[index], ...this.context };
}
}
}

referenceType = this.getReferenceType(typeReference.typeName as ts.EntityName, this.extractEnum, typeReference.typeArguments);

this.current.AddReferenceType(referenceType);

// We do a hard assert in the test mode so we can catch bad ref names (https://github.com/lukeautry/tsoa/issues/398).
Expand Down Expand Up @@ -597,41 +610,13 @@ export class TypeResolver {
throw new GenerateMetadataError(`No valid type found for property declaration.`);
}

// Declare a variable that can be overridden if needed
let aType = propertyDeclaration.type;

// aType.kind will always be a TypeReference when the property of Interface<T> is of type T
if (aType.kind === ts.SyntaxKind.TypeReference && genericTypes && genericTypes.length && node.typeParameters) {
// The type definitions are conviently located on the object which allow us to map -> to the genericTypes
const typeParams = map(node.typeParameters, (typeParam: ts.TypeParameterDeclaration) => {
return typeParam.name.text;
});

// I am not sure in what cases
const typeIdentifier = (aType as ts.TypeReferenceNode).typeName;
let typeIdentifierName: string;

// typeIdentifier can either be a Identifier or a QualifiedName
if ((typeIdentifier as ts.Identifier).text) {
typeIdentifierName = (typeIdentifier as ts.Identifier).text;
} else {
typeIdentifierName = (typeIdentifier as ts.QualifiedName).right.text;
}

// I could not produce a situation where this did not find it so its possible this check is irrelevant
const indexOfType = indexOf(typeParams, typeIdentifierName);
if (indexOfType >= 0) {
aType = genericTypes[indexOfType] as ts.TypeNode;
}
}

return {
default: getJSDocComment(propertyDeclaration, 'default'),
description: this.getNodeDescription(propertyDeclaration),
format: this.getNodeFormat(propertyDeclaration),
name: identifier.text,
required: !propertyDeclaration.questionToken,
type: new TypeResolver(aType, this.current, aType.parent).resolve(),
type: new TypeResolver(propertyDeclaration.type, this.current, propertyDeclaration.type.parent, this.extractEnum, this.context).resolve(),
validators: getPropertyValidators(propertyDeclaration),
} as Tsoa.Property;
});
Expand Down Expand Up @@ -695,7 +680,7 @@ export class TypeResolver {
throw new GenerateMetadataError(`No valid type found for property declaration.`);
}

const type = new TypeResolver(typeNode, this.current, property).resolve();
const type = new TypeResolver(typeNode, this.current, property, this.extractEnum, this.context).resolve();

return {
default: getInitializerValue(property.initializer, type),
Expand All @@ -718,12 +703,12 @@ export class TypeResolver {
}

const indexSignatureDeclaration = indexMember as ts.IndexSignatureDeclaration;
const indexType = new TypeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current).resolve();
const indexType = new TypeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current, this.parentNode, this.extractEnum, this.context).resolve();
if (indexType.dataType !== 'string') {
throw new GenerateMetadataError(`Only string indexers are supported.`);
}

return new TypeResolver(indexSignatureDeclaration.type as ts.TypeNode, this.current).resolve();
return new TypeResolver(indexSignatureDeclaration.type as ts.TypeNode, this.current, this.parentNode, this.extractEnum, this.context).resolve();
}

return undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/routeGeneration/routeGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as fs from 'fs'
import * as fs from 'fs';
import * as handlebars from 'handlebars';
import * as path from 'path';
import * as tsfmt from 'typescript-formatter';
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/testModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,4 +443,6 @@ export interface GenericModel<T> {
export interface GenericRequest<T> {
name: string;
value: T;
union?: T | string;
nested?: GenericRequest<T>;
}

0 comments on commit 0b7d18c

Please sign in to comment.