Repository: nestjs/swagger
Pull request #2227 (2023-01-05 15:07:28)

feat: intersect more than 2 classes
## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?
IntersectionType is only two classes are allowed

Issue Number: N/A


## What is the new behavior?
Modified with reference to [This PR](https://github.com/nestjs/mapped-types/pull/871)

## Does this PR introduce a breaking change?
- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information



Patch:
diff --git a/lib/type-helpers/intersection-type.helper.ts b/lib/type-helpers/intersection-type.helper.ts
index 1a2133c9..734bf39a 100644
--- a/lib/type-helpers/intersection-type.helper.ts
+++ b/lib/type-helpers/intersection-type.helper.ts
@@ -2,7 +2,8 @@ import { Type } from '@nestjs/common';
 import {
   inheritTransformationMetadata,
   inheritValidationMetadata,
-  inheritPropertyInitializers
+  inheritPropertyInitializers,
+  MappedType
 } from '@nestjs/mapped-types';
 import { DECORATORS } from '../constants';
 import { ApiProperty } from '../decorators';
@@ -11,59 +12,57 @@ import { clonePluginMetadataFactory } from './mapped-types.utils';
 
 const modelPropertiesAccessor = new ModelPropertiesAccessor();
 
-export function IntersectionType<A, B>(
-  classARef: Type<A>,
-  classBRef: Type<B>
-): Type<A & B> {
-  const fieldsOfA = modelPropertiesAccessor.getModelProperties(
-    classARef.prototype
-  );
-  const fieldsOfB = modelPropertiesAccessor.getModelProperties(
-    classBRef.prototype
-  );
+type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
+  k: infer I,
+) => void
+  ? I
+  : never;
 
-  abstract class IntersectionTypeClass {
+type ClassRefsToConstructors<T extends Type[]> = {
+  [U in keyof T]: T[U] extends Type<infer V> ? V : never;
+};
+
+type Intersection<T extends Type[]> = MappedType<
+  UnionToIntersection<ClassRefsToConstructors<T>[number]>
+>;
+
+export function IntersectionType<T extends Type[]>(...classRefs: T) {
+  abstract class IntersectionClassType {
     constructor() {
-      inheritPropertyInitializers(this, classARef);
-      inheritPropertyInitializers(this, classBRef);
+      classRefs.forEach((classRef) => {
+        inheritPropertyInitializers(this, classRef);
+      });
     }
   }
-  inheritValidationMetadata(classARef, IntersectionTypeClass);
-  inheritTransformationMetadata(classARef, IntersectionTypeClass);
-  inheritValidationMetadata(classBRef, IntersectionTypeClass);
-  inheritTransformationMetadata(classBRef, IntersectionTypeClass);
-
-  clonePluginMetadataFactory(
-    IntersectionTypeClass as Type<unknown>,
-    classARef.prototype
-  );
-  clonePluginMetadataFactory(
-    IntersectionTypeClass as Type<unknown>,
-    classBRef.prototype
-  );
 
-  fieldsOfA.forEach((propertyKey) => {
-    const metadata = Reflect.getMetadata(
-      DECORATORS.API_MODEL_PROPERTIES,
-      classARef.prototype,
-      propertyKey
+  classRefs.forEach((classRef) => {
+    const fields = modelPropertiesAccessor.getModelProperties(
+      classRef.prototype
     );
-    const decoratorFactory = ApiProperty(metadata);
-    decoratorFactory(IntersectionTypeClass.prototype, propertyKey);
-  });
+  
+    inheritValidationMetadata(classRef, IntersectionClassType);
+    inheritTransformationMetadata(classRef, IntersectionClassType);
 
-  fieldsOfB.forEach((propertyKey) => {
-    const metadata = Reflect.getMetadata(
-      DECORATORS.API_MODEL_PROPERTIES,
-      classBRef.prototype,
-      propertyKey
+    clonePluginMetadataFactory(
+      IntersectionClassType as Type<unknown>,
+      classRef.prototype
     );
-    const decoratorFactory = ApiProperty(metadata);
-    decoratorFactory(IntersectionTypeClass.prototype, propertyKey);
+
+    fields.forEach((propertyKey) => {
+      const metadata = Reflect.getMetadata(
+        DECORATORS.API_MODEL_PROPERTIES,
+        classRef.prototype,
+        propertyKey
+      );
+      const decoratorFactory = ApiProperty(metadata);
+      decoratorFactory(IntersectionClassType.prototype, propertyKey);
+    });
+  
   });
 
-  Object.defineProperty(IntersectionTypeClass, 'name', {
-    value: `Intersection${classARef.name}${classBRef.name}`
+  const intersectedNames = classRefs.reduce((prev, ref) => prev + ref.name, '');
+  Object.defineProperty(IntersectionClassType, 'name', {
+    value: `Intersection${intersectedNames}`,
   });
-  return IntersectionTypeClass as Type<A & B>;
+  return IntersectionClassType as Intersection<T>;
 }


Test patch:
diff --git a/test/type-helpers/intersection-type.helper.spec.ts b/test/type-helpers/intersection-type.helper.spec.ts
index 19911f8b..ce94d030 100644
--- a/test/type-helpers/intersection-type.helper.spec.ts
+++ b/test/type-helpers/intersection-type.helper.spec.ts
@@ -1,6 +1,6 @@
 import { Type } from '@nestjs/common';
 import { Expose, Transform } from 'class-transformer';
-import { IsString } from 'class-validator';
+import { IsBoolean, IsString } from 'class-validator';
 import { ApiProperty } from '../../lib/decorators';
 import { METADATA_FACTORY_NAME } from '../../lib/plugin/plugin-constants';
 import { ModelPropertiesAccessor } from '../../lib/services/model-properties-accessor';
@@ -32,7 +32,17 @@ describe('IntersectionType', () => {
     }
   }
 
-  class UpdateUserDto extends IntersectionType(UserDto, CreateUserDto) {}
+  class AuthorityDto {
+    @IsBoolean()
+    @ApiProperty({ required: true })
+    isAdmin: boolean;
+
+    static [METADATA_FACTORY_NAME]() {
+      return { dateOfBirth3: { required: true, type: () => String } };
+    }
+  }
+
+  class UpdateUserDto extends IntersectionType(UserDto, CreateUserDto, AuthorityDto) {}
 
   let modelPropertiesAccessor: ModelPropertiesAccessor;
 
@@ -49,8 +59,10 @@ describe('IntersectionType', () => {
         'firstName',
         'login',
         'password',
+        'isAdmin',
         'dateOfBirth2',
-        'dateOfBirth'
+        'dateOfBirth',
+        'dateOfBirth3'
       ]);
     });
   });