Skip to content
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

Add support for typeof keyword in type parameters #1753

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 82 additions & 1 deletion packages/cli/src/metadataGeneration/typeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,87 @@ export class TypeResolver {
return new TypeResolver(this.typeNode.type, this.current, this.typeNode, this.context, this.referencer).resolve();
}

if (ts.isTypeQueryNode(this.typeNode)) {
const isIntrinsicType = (type: ts.Type): boolean => {
const flags = ts.TypeFlags;
return (
this.hasFlag(type, flags.Number) ||
this.hasFlag(type, flags.String) ||
this.hasFlag(type, flags.Boolean) ||
this.hasFlag(type, flags.BigInt) ||
this.hasFlag(type, flags.ESSymbol) ||
this.hasFlag(type, flags.Undefined) ||
this.hasFlag(type, flags.Null)
);
};
const symbol = this.current.typeChecker.getSymbolAtLocation(this.typeNode.exprName);
if (symbol) {
// Access the first declaration of the symbol
const declaration = symbol.declarations?.[0];
throwUnless(declaration, new GenerateMetadataError(`Could not find declaration for symbol: ${symbol.name}`, this.typeNode));

if (ts.isVariableDeclaration(declaration)) {
const initializer = declaration.initializer;
const declarationType = this.current.typeChecker.getTypeAtLocation(declaration);
if (isIntrinsicType(declarationType)) {
return { dataType: this.current.typeChecker.typeToString(declarationType) as Tsoa.PrimitiveType['dataType'] };
}
if (initializer && (ts.isStringLiteral(initializer) || ts.isNumericLiteral(initializer) || ts.isBigIntLiteral(initializer))) {
return { dataType: 'enum', enums: [initializer.text] };
} else if (initializer && ts.isObjectLiteralExpression(initializer)) {
const getOneOrigDeclaration = (prop: ts.Symbol): ts.Declaration | undefined => {
if (prop.declarations) {
return prop.declarations[0];
}
const syntheticOrigin: ts.Symbol = (prop as any).links?.syntheticOrigin;
if (syntheticOrigin && syntheticOrigin.name === prop.name) {
return syntheticOrigin.declarations?.[0];
}
return undefined;
};

const isIgnored = (prop: ts.Symbol) => {
const declaration = getOneOrigDeclaration(prop);
return declaration !== undefined && getJSDocTagNames(declaration).some(tag => tag === 'ignore') && !ts.isPropertyAssignment(declaration);
};

const typeProperties: ts.Symbol[] = this.current.typeChecker.getPropertiesOfType(this.current.typeChecker.getTypeAtLocation(initializer));
const properties: Tsoa.Property[] = typeProperties
Copy link
Collaborator

Choose a reason for hiding this comment

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

A lot of the property management should already exist in checker based resolutions.

Ideally we can merge them together.

.filter(property => isIgnored(property) === false)
.map(property => {
const propertyType = this.current.typeChecker.getTypeOfSymbolAtLocation(property, this.typeNode);
const typeNode = this.current.typeChecker.typeToTypeNode(propertyType, undefined, ts.NodeBuilderFlags.NoTruncation)!;
const parent = getOneOrigDeclaration(property);
const type = new TypeResolver(typeNode, this.current, parent, this.context, propertyType).resolve();

const required = !this.hasFlag(property, ts.SymbolFlags.Optional);
const comments = property.getDocumentationComment(this.current.typeChecker);
const description = comments.length ? ts.displayPartsToString(comments) : undefined;

return {
name: property.getName(),
required,
deprecated: parent ? isExistJSDocTag(parent, tag => tag.tagName.text === 'deprecated') || isDecorator(parent, identifier => identifier.text === 'Deprecated') : false,
type,
default: undefined,
validators: (parent ? getPropertyValidators(parent) : {}) || {},
description,
format: parent ? this.getNodeFormat(parent) : undefined,
example: parent ? this.getNodeExample(parent) : undefined,
extensions: parent ? this.getNodeExtension(parent) : undefined,
};
});

const objectLiteral: Tsoa.NestedObjectLiteralType = {
dataType: 'nestedObjectLiteral',
properties,
};
return objectLiteral;
}
}
}
}

throwUnless(this.typeNode.kind === ts.SyntaxKind.TypeReference, new GenerateMetadataError(`Unknown type: ${ts.SyntaxKind[this.typeNode.kind]}`, this.typeNode));

return this.resolveTypeReferenceNode(this.typeNode as ts.TypeReferenceNode, this.current, this.context, this.parentNode);
Expand Down Expand Up @@ -573,7 +654,7 @@ export class TypeResolver {
if (this.context[name]) {
//resolve name only interesting if entity is not qualifiedName
name = this.context[name].name; //Not needed to check unicity, because generic parameters are checked previously
} else {
} else if (type.parent && !ts.isTypeQueryNode(type.parent)) {
const declarations = this.getModelTypeDeclarations(type);

//Two possible solutions for recognizing different types:
Expand Down
7 changes: 6 additions & 1 deletion tests/fixtures/controllers/parameterController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Body, BodyProp, Get, Header, Path, Post, Query, Request, Route, Res, TsoaResponse, Deprecated, Queries, RequestProp, FormField } from '@tsoa/runtime';
import { Gender, ParameterTestModel } from '../testModel';
import { Gender, ParameterTestModel, ValueType } from '../testModel';

@Route('ParameterTest')
export class ParameterController {
Expand Down Expand Up @@ -402,4 +402,9 @@ export class ParameterController {
public async inline1(@Body() body: { requestString: string; requestNumber: number }): Promise<{ resultString: string; responseNumber: number }> {
return { resultString: 'a', responseNumber: 1 };
}

@Post('TypeInference')
public async typeInference(@Body() body: ValueType): Promise<ValueType> {
return { a: 'a', b: 1 };
}
}
4 changes: 4 additions & 0 deletions tests/fixtures/testModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1289,3 +1289,7 @@ type OrderDirection = 'asc' | 'desc';
type OrderOptions<E> = `${keyof E & string}:${OrderDirection}`;

type TemplateLiteralString = OrderOptions<ParameterTestModel>;

const value = { a: 'a', b: 1 };
type Infer<T> = T;
export type ValueType = Infer<typeof value>;
17 changes: 17 additions & 0 deletions tests/unit/swagger/definitionsGeneration/metadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,23 @@ describe('Metadata generation', () => {
const deprecatedParam2 = method.parameters[2];
expect(deprecatedParam2.deprecated).to.be.true;
});

it('should handle type inference params', () => {
const method = controller.methods.find(m => m.name === 'typeInference');
if (!method) {
throw new Error('Method typeInference not defined!');
}
const parameter = method.parameters.find(param => param.parameterName === 'body');
if (!parameter) {
throw new Error('Parameter firstname not defined!');
}

expect(method.parameters.length).to.equal(1);
expect(parameter.in).to.equal('body');
expect(parameter.name).to.equal('body');
expect(parameter.parameterName).to.equal('body');
expect(parameter.required).to.be.true;
});
});

describe('HiddenMethodGenerator', () => {
Expand Down
Loading
Loading