diff --git a/lib/plugin/utils/plugin-utils.ts b/lib/plugin/utils/plugin-utils.ts index f68fc4e7f..15db767c4 100644 --- a/lib/plugin/utils/plugin-utils.ts +++ b/lib/plugin/utils/plugin-utils.ts @@ -62,11 +62,9 @@ export function getTypeReferenceAsString( if (text === Date.name) { return text; } - if (isAutoGeneratedUnion(type)) { - return getTypeReferenceAsString( - type.types[type.types.length - 1], - typeChecker - ); + if (isAutoGeneratedUnion(type, typeChecker)) { + const types = (type as ts.UnionOrIntersectionType).types; + return getTypeReferenceAsString(types[types.length - 1], typeChecker); } if ( text === 'any' || @@ -151,13 +149,44 @@ export function isDynamicallyAdded(identifier: ts.Node) { * @param type */ export function isAutoGeneratedUnion( - type: ts.Type -): type is ts.UnionOrIntersectionType { + type: ts.Type, + typeChecker: ts.TypeChecker +): boolean | ts.Type { if (type.isUnionOrIntersection() && !isEnum(type)) { - if (type.types && type.types.length === 2) { - if (type.types.some((type: any) => type.intrinsicName === 'undefined')) { + if (!type.types) { + return false; + } + const undefinedTypeIndex = type.types.findIndex( + (type: any) => type.intrinsicName === 'undefined' + ); + + // "strict" mode for enums + let parentType = undefined; + const isParentSymbolEqual = type.types.every((item, index) => { + if (index === undefinedTypeIndex) { return true; } + if (!item.symbol) { + return false; + } + if (!(item.symbol as any).parent) { + return false; + } + const symbolType = typeChecker.getDeclaredTypeOfSymbol( + (item.symbol as any).parent + ); + if (symbolType === parentType || !parentType) { + parentType = symbolType; + return true; + } + return false; + }); + if (isParentSymbolEqual) { + return parentType; + } + // "strict" mode for non-enum properties + if (type.types.length === 2 && undefinedTypeIndex >= 0) { + return true; } } return false; diff --git a/lib/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts index 34271fc8a..0466bfc8f 100644 --- a/lib/plugin/visitors/model-class.visitor.ts +++ b/lib/plugin/visitors/model-class.visitor.ts @@ -14,6 +14,7 @@ import { getDecoratorOrUndefinedByNames, getTypeReferenceAsString, hasPropertyKey, + isAutoGeneratedUnion, replaceImportPath } from '../utils/plugin-utils'; import { AbstractFileVisitor } from './abstract.visitor'; @@ -237,7 +238,12 @@ export class ModelClassVisitor extends AbstractFileVisitor { } } if (!isEnum(type)) { - return undefined; + const typeOrBool = isAutoGeneratedUnion(type, typeChecker); + if (typeof typeOrBool !== 'boolean') { + type = typeOrBool; + } else { + return undefined; + } } const enumRef = replaceImportPath(getText(type, typeChecker), hostFilename); const enumProperty = ts.createPropertyAssignment( diff --git a/test/plugin/fixtures/create-cat.dto.ts b/test/plugin/fixtures/create-cat.dto.ts index 14c67c90c..0a3359d20 100644 --- a/test/plugin/fixtures/create-cat.dto.ts +++ b/test/plugin/fixtures/create-cat.dto.ts @@ -17,6 +17,7 @@ export class CreateCatDto { age: number = 3; tags: string[]; status: Status = Status.ENABLED; + status2?: Status; @ApiProperty({ type: String }) @IsString() @@ -45,7 +46,7 @@ export class CreateCatDto { this.status = Status.ENABLED; } static _OPENAPI_METADATA_FACTORY() { - return { name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, breed: { required: false, type: () => String }, nodes: { required: true, type: () => [Object] }, date: { required: true, type: () => Date } }; + return { name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, status2: { required: false, enum: Status }, breed: { required: false, type: () => String }, nodes: { required: true, type: () => [Object] }, date: { required: true, type: () => Date } }; } } __decorate([ diff --git a/test/plugin/model-class-visitor.spec.ts b/test/plugin/model-class-visitor.spec.ts index 5aa317e33..08ad29945 100644 --- a/test/plugin/model-class-visitor.spec.ts +++ b/test/plugin/model-class-visitor.spec.ts @@ -23,7 +23,8 @@ describe('API model properties', () => { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext, newLine: ts.NewLineKind.LineFeed, - noEmitHelpers: true + noEmitHelpers: true, + strict: true }; const filename = 'create-cat.dto.ts'; const fakeProgram = ts.createProgram([filename], options); @@ -43,7 +44,8 @@ describe('API model properties', () => { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext, newLine: ts.NewLineKind.LineFeed, - noEmitHelpers: true + noEmitHelpers: true, + strict: true }; const filename = 'create-cat.dto.ts'; const fakeProgram = ts.createProgram([filename], options); @@ -63,7 +65,8 @@ describe('API model properties', () => { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext, newLine: ts.NewLineKind.LineFeed, - noEmitHelpers: true + noEmitHelpers: true, + strict: true }; const filename = 'create-cat-alt2.dto.ts'; const fakeProgram = ts.createProgram([filename], options); @@ -83,7 +86,8 @@ describe('API model properties', () => { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES5, newLine: ts.NewLineKind.LineFeed, - noEmitHelpers: true + noEmitHelpers: true, + strict: true }; const filename = 'es5-class.dto.ts'; const fakeProgram = ts.createProgram([filename], options);