From c15b393f85e99f9d6c8cbdaea8209109c08cc1d8 Mon Sep 17 00:00:00 2001 From: Patrick Clements Date: Mon, 3 Mar 2025 16:09:26 -0500 Subject: [PATCH 1/4] add support for typeof references, add typeresolver unit test --- .../src/metadataGeneration/typeResolver.ts | 83 ++- .../controllers/parameterController.ts | 7 +- tests/fixtures/testModel.ts | 3 + .../definitionsGeneration/metadata.spec.ts | 17 + .../typeResolver.spec.ts | 527 ++++++++++++++++++ 5 files changed, 635 insertions(+), 2 deletions(-) create mode 100644 tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts diff --git a/packages/cli/src/metadataGeneration/typeResolver.ts b/packages/cli/src/metadataGeneration/typeResolver.ts index 315854b38..476e8daae 100644 --- a/packages/cli/src/metadataGeneration/typeResolver.ts +++ b/packages/cli/src/metadataGeneration/typeResolver.ts @@ -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 + .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); @@ -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: diff --git a/tests/fixtures/controllers/parameterController.ts b/tests/fixtures/controllers/parameterController.ts index 64ff6517b..3b1c512cf 100644 --- a/tests/fixtures/controllers/parameterController.ts +++ b/tests/fixtures/controllers/parameterController.ts @@ -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, TypeOfLiteral } from '../testModel'; @Route('ParameterTest') export class ParameterController { @@ -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: TypeOfLiteral): Promise { + return { a: 'a', b: 1 }; + } } diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts index 4cf58b56b..a052ee110 100644 --- a/tests/fixtures/testModel.ts +++ b/tests/fixtures/testModel.ts @@ -1289,3 +1289,6 @@ type OrderDirection = 'asc' | 'desc'; type OrderOptions = `${keyof E & string}:${OrderDirection}`; type TemplateLiteralString = OrderOptions; + +const concrete = { a: 'a', b: 1 }; +export type TypeOfLiteral = typeof concrete; diff --git a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts index 878d105e6..7f8b6f079 100644 --- a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts +++ b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts @@ -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', () => { diff --git a/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts b/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts new file mode 100644 index 000000000..3411b540c --- /dev/null +++ b/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts @@ -0,0 +1,527 @@ +import { expect } from 'chai'; +import * as ts from 'typescript'; +import { TypeResolver } from '@tsoa/cli/metadataGeneration/typeResolver'; +import { MetadataGenerator } from '@tsoa/cli/metadataGeneration/metadataGenerator'; +import { z } from 'zod'; +import { ref } from 'joi'; + +const refAliasProperties = { + default: undefined, + description: undefined, + format: undefined, + validators: {}, +}; + +const defaultProperties = { + default: undefined, + deprecated: false, + description: undefined, + example: undefined, + extensions: [], + format: undefined, + validators: {}, +}; + +function createProgramFromSource(source: string, fileName: string = 'test.ts') { + const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest); + + const defaultCompilerHost = ts.createCompilerHost({}); + const compilerHost = ts.createCompilerHost({}); + compilerHost.getSourceFile = (fileName, languageVersion) => { + if (fileName === sourceFile.fileName) { + return sourceFile; + } + return defaultCompilerHost.getSourceFile(fileName, languageVersion); + }; + compilerHost.fileExists = fileName => { + if (fileName === sourceFile.fileName) { + return true; + } + return defaultCompilerHost.fileExists(fileName); + }; + + const program = ts.createProgram([sourceFile.fileName], {}, compilerHost); + const metadataGenerator = new MetadataGenerator(fileName); + (metadataGenerator as any).program = program; + (metadataGenerator as any).typeChecker = program.getTypeChecker(); + + const typeAliasDeclaration = sourceFile.statements.filter(ts.isTypeAliasDeclaration).pop(); + if (!typeAliasDeclaration) { + throw new Error('No type alias declaration found in source'); + } + + return { + metadataGenerator, + typeNode: typeAliasDeclaration.type, + }; +} + +describe('TypeResolver.resolve', () => { + it('should resolve an object type', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type ObjectType = { a: string; b: number; };'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'nestedObjectLiteral', + properties: [ + { + name: 'b', + type: { dataType: 'double' }, + required: true, + ...defaultProperties, + }, + { + name: 'a', + type: { dataType: 'string' }, + required: true, + ...defaultProperties, + }, + ], + additionalProperties: undefined, + }); + }); + + it('should resolve conditional types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type ConditionalType = T extends string ? string : number; type Concrete = ConditionalType;'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + refName: 'ConditionalType_boolean_', + dataType: 'refAlias', + type: { dataType: 'double' }, + ...refAliasProperties, + }); + }); + + it('should resolve indexed access types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type IndexedAccessType = { a: string }["a"];'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ dataType: 'string' }); + }); + + it('should resolve inferred types', () => { + const source = 'type InferredType = T extends infer U ? U : never;\n' + 'type Concrete = InferredType;'; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + refName: 'InferredType_string_', + dataType: 'refAlias', + type: { dataType: 'string' }, + ...refAliasProperties, + }); + }); + + it('should resolve typeof with literal', () => { + const source = ` + const hello = "hello" + type InferredType = typeof hello; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'enum', + enums: ['hello'], + }); + }); + + it('should resolve typeof with object', () => { + const source = ` + const concrete = {a: "a", b: 1}; + type InferredType = typeof concrete; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'nestedObjectLiteral', + properties: [ + { + name: 'a', + type: { dataType: 'string' }, + required: true, + ...defaultProperties, + }, + { + name: 'b', + type: { dataType: 'double' }, + required: true, + ...defaultProperties, + }, + ], + }); + }); + + it('should resolve intersection types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type IntersectionType = { a: string } & { b: number };'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'intersection', + types: [ + { + additionalProperties: undefined, + dataType: 'nestedObjectLiteral', + properties: [ + { + name: 'a', + type: { dataType: 'string' }, + required: true, + ...defaultProperties, + }, + ], + }, + { + additionalProperties: undefined, + dataType: 'nestedObjectLiteral', + properties: [ + { + name: 'b', + type: { dataType: 'double' }, + required: true, + ...defaultProperties, + }, + ], + }, + ], + }); + }); + + it('should resolve intrinsic types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type IntrinsicType = string;'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ dataType: 'string' }); + }); + + it('should resolve literal types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type LiteralType = "literal";'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ dataType: 'enum', enums: ['literal'] }); + }); + + it('should resolve mapped types', () => { + const source = 'type MappedType = { [P in keyof T]: T[P] }; \n' + 'type Concrete = MappedType<{ a: string; b: number }>;'; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'refAlias', + refName: 'MappedType__a-string--b-number__', + type: { + dataType: 'nestedObjectLiteral', + properties: [ + { + name: 'a', + type: { dataType: 'string' }, + required: true, + ...defaultProperties, + }, + { + name: 'b', + type: { dataType: 'double' }, + required: true, + ...defaultProperties, + }, + ], + }, + ...refAliasProperties, + }); + }); + + it('should resolve mapped types with contraints', () => { + const source = ` + type Flatten> = { + [k in keyof T]: T[k]; + }; + + type ValueType = Flatten<{ name: string }>; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'refAlias', + refName: 'Flatten__name-string__', + type: { + dataType: 'nestedObjectLiteral', + properties: [ + { + name: 'name', + type: { dataType: 'string' }, + required: true, + ...defaultProperties, + }, + ], + }, + ...refAliasProperties, + }); + }); + + it('should resolve optional types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type OptionalType = { a?: string };'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'nestedObjectLiteral', + properties: [ + { + name: 'a', + type: { dataType: 'string' }, + required: false, + ...defaultProperties, + }, + ], + additionalProperties: undefined, + }); + }); + + it('should resolve predicate types', () => { + const source = ` + type PredicateType = (x: any) => x is string; + type Concrete = ReturnType; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'refAlias', + refName: 'ReturnType_PredicateType_', + type: { dataType: 'boolean' }, + ...refAliasProperties, + description: 'Obtain the return type of a function type', + }); + }); + + it('should resolve keyof query types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type QueryType = keyof { a: string };'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ dataType: 'enum', enums: ['a'] }); + }); + + it('should resolve reference types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type ReferenceType = { a: string };'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'nestedObjectLiteral', + properties: [ + { + name: 'a', + type: { dataType: 'string' }, + required: true, + ...defaultProperties, + }, + ], + additionalProperties: undefined, + }); + }); + + it.skip('should resolve nested interface types', () => { + const source = ` + interface Address { line1: string; line2?: string; postalCode: string; }; + type Person = { + first: string; + last: string; + address: Address; + }; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'nestedObjectLiteral', + properties: [ + { + name: 'a', + type: { dataType: 'string' }, + required: true, + ...defaultProperties, + }, + ], + additionalProperties: undefined, + }); + }); + + it('should resolve rest types', () => { + const source = 'type Sum = (...numbers: number[]) => number;\n' + 'type RestType = Parameters;'; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'refAlias', + refName: 'Parameters_Sum_', + type: { + dataType: 'array', + elementType: { + dataType: 'double', + }, + }, + ...refAliasProperties, + description: 'Obtain the parameters of a function type in a tuple', + }); + }); + + /** + * Tuple types aren't supported in Open API specs < 3.1 + */ + it.skip('should resolve tuple types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type TupleType = [string, number];'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + // expect(result).to.deep.equal({ dataType: 'tuple', elementTypes: [{ dataType: 'string' }, { dataType: 'double' }] }); + }); + + it.skip('should resolve named tuple member types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type NamedTupleMemberType = [a: string, b: number];'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + // expect(result).to.deep.equal({ dataType: 'tuple', elementTypes: [{ dataType: 'string' }, { dataType: 'double' }] }); + }); + + // Only template literal types with literal string unions are supported + it('should resolve template literal types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type TemplateLiteralType = `prefix_${"a" | "b"}`;'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ dataType: 'enum', enums: ['prefix_a', 'prefix_b'] }); + }); + + it('should resolve type operator types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type TypeOperatorType = keyof { a: string };'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ dataType: 'enum', enums: ['a'] }); + }); + + it('should resolve union types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type UnionType = string | number;'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ dataType: 'union', types: [{ dataType: 'string' }, { dataType: 'double' }] }); + }); + + it('should resolve unknown types', () => { + const { metadataGenerator, typeNode } = createProgramFromSource('type UnknownType = unknown;'); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ dataType: 'any' }); + }); + + it('should resolve simple generic types', () => { + const source = ` + type Wrapper = { data: T }; + type Concrete = Wrapper; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'refAlias', + refName: 'Wrapper_string_', + type: { + dataType: 'nestedObjectLiteral', + properties: [{ name: 'data', type: { dataType: 'string' }, required: true, ...defaultProperties }], + additionalProperties: undefined, + }, + ...refAliasProperties, + }); + }); + + it.skip('should resolve indexed generic types', () => { + const source = ` + type Type = { _type: Output }; + type ValueType = Type["_type"]; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'nestedObjectLiteral', + properties: [ + { name: 'a', type: { dataType: 'string' }, required: true, ...defaultProperties }, + { name: 'b', type: { dataType: 'double' }, required: true, ...defaultProperties }, + ], + additionalProperties: undefined, + }); + }); + + it.skip('should resolve Zod inferred types', () => { + const source = ` + import { z } from 'zod'; + const schema = z.object({ a: z.string(), b: z.number() }); + type InferredType = z.infer; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'nestedObjectLiteral', + properties: [ + { name: 'a', type: { dataType: 'string' }, required: true, ...defaultProperties }, + { name: 'b', type: { dataType: 'double' }, required: true, ...defaultProperties }, + ], + additionalProperties: undefined, + }); + }); + + it('should resolve references to inferred types with intrinsic', () => { + const source = ` + const value: string = "a"; + type Infer = T; + type ValueType = Infer; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'refAlias', + refName: 'Infer_typeofvalue_', + type: { dataType: 'string' }, + ...refAliasProperties, + }); + }); + + it('should resolve references to inferred types with literal', () => { + const source = ` + const value = "a"; + type Infer = T; + type ValueType = Infer; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'refAlias', + refName: 'Infer_typeofvalue_', + type: { dataType: 'enum', enums: ['a'] }, + ...refAliasProperties, + }); + }); + it('should resolve references to inferred types with object', () => { + const source = ` + const value = { a: "a", b: 1}; + type Infer = T; + type ValueType = Infer; + `; + const { metadataGenerator, typeNode } = createProgramFromSource(source); + const resolver = new TypeResolver(typeNode, metadataGenerator); + const result = resolver.resolve(); + expect(result).to.deep.equal({ + dataType: 'refAlias', + refName: 'Infer_typeofvalue_', + type: { + dataType: 'nestedObjectLiteral', + properties: [ + { name: 'a', type: { dataType: 'string' }, required: true, ...defaultProperties }, + { name: 'b', type: { dataType: 'double' }, required: true, ...defaultProperties }, + ], + }, + ...refAliasProperties, + }); + }); +}); From 01c77047a1691592da6d7e5b1e3c378c4d8d43cf Mon Sep 17 00:00:00 2001 From: Patrick Clements Date: Mon, 3 Mar 2025 16:27:14 -0500 Subject: [PATCH 2/4] clean-up typeResolver test --- .../typeResolver.spec.ts | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts b/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts index 3411b540c..6f824543d 100644 --- a/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts +++ b/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts @@ -2,8 +2,6 @@ import { expect } from 'chai'; import * as ts from 'typescript'; import { TypeResolver } from '@tsoa/cli/metadataGeneration/typeResolver'; import { MetadataGenerator } from '@tsoa/cli/metadataGeneration/metadataGenerator'; -import { z } from 'zod'; -import { ref } from 'joi'; const refAliasProperties = { default: undefined, @@ -321,6 +319,7 @@ describe('TypeResolver.resolve', () => { }); }); + // works with nested types, but not with interfaces it.skip('should resolve nested interface types', () => { const source = ` interface Address { line1: string; line2?: string; postalCode: string; }; @@ -366,23 +365,6 @@ describe('TypeResolver.resolve', () => { }); }); - /** - * Tuple types aren't supported in Open API specs < 3.1 - */ - it.skip('should resolve tuple types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type TupleType = [string, number];'); - const resolver = new TypeResolver(typeNode, metadataGenerator); - const result = resolver.resolve(); - // expect(result).to.deep.equal({ dataType: 'tuple', elementTypes: [{ dataType: 'string' }, { dataType: 'double' }] }); - }); - - it.skip('should resolve named tuple member types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type NamedTupleMemberType = [a: string, b: number];'); - const resolver = new TypeResolver(typeNode, metadataGenerator); - const result = resolver.resolve(); - // expect(result).to.deep.equal({ dataType: 'tuple', elementTypes: [{ dataType: 'string' }, { dataType: 'double' }] }); - }); - // Only template literal types with literal string unions are supported it('should resolve template literal types', () => { const { metadataGenerator, typeNode } = createProgramFromSource('type TemplateLiteralType = `prefix_${"a" | "b"}`;'); @@ -432,6 +414,8 @@ describe('TypeResolver.resolve', () => { }); }); + // One of the issues that blocks zod's z.infer from working as expected + // see https://github.com/lukeautry/tsoa/issues/1256#issuecomment-2649340573 it.skip('should resolve indexed generic types', () => { const source = ` type Type = { _type: Output }; @@ -450,6 +434,7 @@ describe('TypeResolver.resolve', () => { }); }); + // Note: requires installing zod as a dependency as well it.skip('should resolve Zod inferred types', () => { const source = ` import { z } from 'zod'; From f3f48bed2405b9d68d0c61d2fc224c0b93bee028 Mon Sep 17 00:00:00 2001 From: Patrick Clements Date: Tue, 4 Mar 2025 09:04:07 -0500 Subject: [PATCH 3/4] use valueOf in type parameter to be exact --- tests/fixtures/controllers/parameterController.ts | 4 ++-- tests/fixtures/testModel.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/controllers/parameterController.ts b/tests/fixtures/controllers/parameterController.ts index 3b1c512cf..59347d292 100644 --- a/tests/fixtures/controllers/parameterController.ts +++ b/tests/fixtures/controllers/parameterController.ts @@ -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, TypeOfLiteral } from '../testModel'; +import { Gender, ParameterTestModel, ValueType } from '../testModel'; @Route('ParameterTest') export class ParameterController { @@ -404,7 +404,7 @@ export class ParameterController { } @Post('TypeInference') - public async typeInference(@Body() body: TypeOfLiteral): Promise { + public async typeInference(@Body() body: ValueType): Promise { return { a: 'a', b: 1 }; } } diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts index a052ee110..5bc796986 100644 --- a/tests/fixtures/testModel.ts +++ b/tests/fixtures/testModel.ts @@ -1290,5 +1290,6 @@ type OrderOptions = `${keyof E & string}:${OrderDirection}`; type TemplateLiteralString = OrderOptions; -const concrete = { a: 'a', b: 1 }; -export type TypeOfLiteral = typeof concrete; +const value = { a: 'a', b: 1 }; +type Infer = T; +export type ValueType = Infer; From 0468cc38634a5c192e31fdabe014240448b0d381 Mon Sep 17 00:00:00 2001 From: woh Date: Mon, 7 Apr 2025 15:10:25 +0200 Subject: [PATCH 4/4] chore: Inline source syntax highlighting --- .vscode/extensions.json | 7 + .../typeResolver.spec.ts | 156 ++++++++++++------ 2 files changed, 111 insertions(+), 52 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..fee72d99c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "bierner.comment-tagged-templates", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] +} \ No newline at end of file diff --git a/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts b/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts index 6f824543d..e6411a540 100644 --- a/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts +++ b/tests/unit/swagger/definitionsGeneration/typeResolver.spec.ts @@ -20,7 +20,7 @@ const defaultProperties = { validators: {}, }; -function createProgramFromSource(source: string, fileName: string = 'test.ts') { +function createProgramFromSource(source: string, fileName = 'test.ts') { const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest); const defaultCompilerHost = ts.createCompilerHost({}); @@ -40,8 +40,10 @@ function createProgramFromSource(source: string, fileName: string = 'test.ts') { const program = ts.createProgram([sourceFile.fileName], {}, compilerHost); const metadataGenerator = new MetadataGenerator(fileName); - (metadataGenerator as any).program = program; - (metadataGenerator as any).typeChecker = program.getTypeChecker(); + // @ts-expect-error: 'program' is a private and read-only property of MetadataGenerator + metadataGenerator.program = program; + // @ts-expect-error: 'program' is a private and read-only property of MetadataGenerator + metadataGenerator.typeChecker = program.getTypeChecker(); const typeAliasDeclaration = sourceFile.statements.filter(ts.isTypeAliasDeclaration).pop(); if (!typeAliasDeclaration) { @@ -56,7 +58,10 @@ function createProgramFromSource(source: string, fileName: string = 'test.ts') { describe('TypeResolver.resolve', () => { it('should resolve an object type', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type ObjectType = { a: string; b: number; };'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type ObjectType = { a: string; b: number; }; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -80,9 +85,14 @@ describe('TypeResolver.resolve', () => { }); it('should resolve conditional types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type ConditionalType = T extends string ? string : number; type Concrete = ConditionalType;'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type ConditionalType = T extends string ? string : number; + type Concrete = ConditionalType; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); + expect(result).to.deep.equal({ refName: 'ConditionalType_boolean_', dataType: 'refAlias', @@ -92,15 +102,21 @@ describe('TypeResolver.resolve', () => { }); it('should resolve indexed access types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type IndexedAccessType = { a: string }["a"];'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type IndexedAccessType = { a: string }["a"]; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ dataType: 'string' }); }); it('should resolve inferred types', () => { - const source = 'type InferredType = T extends infer U ? U : never;\n' + 'type Concrete = InferredType;'; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type InferredType = T extends infer U ? U : never; + type Concrete = InferredType; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -112,11 +128,11 @@ describe('TypeResolver.resolve', () => { }); it('should resolve typeof with literal', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` const hello = "hello" type InferredType = typeof hello; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -126,11 +142,11 @@ describe('TypeResolver.resolve', () => { }); it('should resolve typeof with object', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` const concrete = {a: "a", b: 1}; type InferredType = typeof concrete; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -153,7 +169,10 @@ describe('TypeResolver.resolve', () => { }); it('should resolve intersection types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type IntersectionType = { a: string } & { b: number };'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type IntersectionType = { a: string } & { b: number }; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -188,22 +207,31 @@ describe('TypeResolver.resolve', () => { }); it('should resolve intrinsic types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type IntrinsicType = string;'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type IntrinsicType = string; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ dataType: 'string' }); }); it('should resolve literal types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type LiteralType = "literal";'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type LiteralType = "literal"; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ dataType: 'enum', enums: ['literal'] }); }); it('should resolve mapped types', () => { - const source = 'type MappedType = { [P in keyof T]: T[P] }; \n' + 'type Concrete = MappedType<{ a: string; b: number }>;'; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type MappedType = { [P in keyof T]: T[P] }; + type Concrete = MappedType<{ a: string; b: number }>; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -260,7 +288,10 @@ describe('TypeResolver.resolve', () => { }); it('should resolve optional types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type OptionalType = { a?: string };'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type OptionalType = { a?: string }; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -278,11 +309,11 @@ describe('TypeResolver.resolve', () => { }); it('should resolve predicate types', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` type PredicateType = (x: any) => x is string; type Concrete = ReturnType; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -295,14 +326,20 @@ describe('TypeResolver.resolve', () => { }); it('should resolve keyof query types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type QueryType = keyof { a: string };'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type QueryType = keyof { a: string }; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ dataType: 'enum', enums: ['a'] }); }); it('should resolve reference types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type ReferenceType = { a: string };'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type ReferenceType = { a: string }; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -321,15 +358,15 @@ describe('TypeResolver.resolve', () => { // works with nested types, but not with interfaces it.skip('should resolve nested interface types', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` interface Address { line1: string; line2?: string; postalCode: string; }; type Person = { first: string; last: string; address: Address; }; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -347,8 +384,11 @@ describe('TypeResolver.resolve', () => { }); it('should resolve rest types', () => { - const source = 'type Sum = (...numbers: number[]) => number;\n' + 'type RestType = Parameters;'; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type Sum = (...numbers: number[]) => number; + type RestType = Parameters; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -367,39 +407,51 @@ describe('TypeResolver.resolve', () => { // Only template literal types with literal string unions are supported it('should resolve template literal types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type TemplateLiteralType = `prefix_${"a" | "b"}`;'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type TemplateLiteralType = \`prefix_\${"a" | "b"}\`; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ dataType: 'enum', enums: ['prefix_a', 'prefix_b'] }); }); it('should resolve type operator types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type TypeOperatorType = keyof { a: string };'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type TypeOperatorType = keyof { a: string }; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ dataType: 'enum', enums: ['a'] }); }); it('should resolve union types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type UnionType = string | number;'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type UnionType = string | number; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ dataType: 'union', types: [{ dataType: 'string' }, { dataType: 'double' }] }); }); it('should resolve unknown types', () => { - const { metadataGenerator, typeNode } = createProgramFromSource('type UnknownType = unknown;'); + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` + type UnknownType = unknown; + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ dataType: 'any' }); }); it('should resolve simple generic types', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` type Wrapper = { data: T }; type Concrete = Wrapper; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -417,11 +469,11 @@ describe('TypeResolver.resolve', () => { // One of the issues that blocks zod's z.infer from working as expected // see https://github.com/lukeautry/tsoa/issues/1256#issuecomment-2649340573 it.skip('should resolve indexed generic types', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` type Type = { _type: Output }; type ValueType = Type["_type"]; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -436,12 +488,12 @@ describe('TypeResolver.resolve', () => { // Note: requires installing zod as a dependency as well it.skip('should resolve Zod inferred types', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` import { z } from 'zod'; const schema = z.object({ a: z.string(), b: z.number() }); type InferredType = z.infer; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -455,12 +507,12 @@ describe('TypeResolver.resolve', () => { }); it('should resolve references to inferred types with intrinsic', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` const value: string = "a"; type Infer = T; type ValueType = Infer; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -472,12 +524,12 @@ describe('TypeResolver.resolve', () => { }); it('should resolve references to inferred types with literal', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` const value = "a"; type Infer = T; type ValueType = Infer; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({ @@ -488,12 +540,12 @@ describe('TypeResolver.resolve', () => { }); }); it('should resolve references to inferred types with object', () => { - const source = ` + const { metadataGenerator, typeNode } = createProgramFromSource(/*ts*/ ` const value = { a: "a", b: 1}; type Infer = T; type ValueType = Infer; - `; - const { metadataGenerator, typeNode } = createProgramFromSource(source); + `); + const resolver = new TypeResolver(typeNode, metadataGenerator); const result = resolver.resolve(); expect(result).to.deep.equal({