From e8d2f4abdeb405393e0b73f1f0caced4b51df538 Mon Sep 17 00:00:00 2001 From: Kong Ki Pan Date: Thu, 29 Sep 2022 17:06:09 +0800 Subject: [PATCH] fix(): intersect more than 4 classes --- lib/intersection-type.helper.ts | 53 +++++++++---------- .../intersection-type-multiple.helper.spec.ts | 24 ++++++++- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/lib/intersection-type.helper.ts b/lib/intersection-type.helper.ts index dd10af03..227622ca 100644 --- a/lib/intersection-type.helper.ts +++ b/lib/intersection-type.helper.ts @@ -1,4 +1,5 @@ import { Type } from '@nestjs/common'; + import { MappedType } from './mapped-type.interface'; import { inheritPropertyInitializers, @@ -6,49 +7,45 @@ import { inheritValidationMetadata, } from './type-helpers.utils'; -export function IntersectionType( - target: Type, - source: Type, -): MappedType; - -export function IntersectionType( - target: Type, - sourceB: Type, - sourceC: Type, -): MappedType; +// https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never; -export function IntersectionType( - target: Type, - sourceB: Type, - sourceC: Type, - sourceD: Type, -): MappedType; +// It converts ClassRefs array `Type[]` to `Class[]` by `infer` +// e.g. `ClassRefsToConstructors<[Type, Type]>` becomes `[Foo, Bar]` +type ClassRefsToConstructors = { + [U in keyof T]: T[U] extends Type ? V : never; +}; -export function IntersectionType( - classA: Type, - ...classRefs: T -): MappedType { - const allClassRefs = [classA, ...classRefs]; +// Firstly, it uses indexed access type `Class[][number]` to convert `Class[]` to union type of it +// e.g. `[Foo, Bar][number]` becomes `Foo | Bar` +// then, it use the `UnionToIntersection` type to transform union type to intersection type +// e.g. `Foo | Bar` becomes `Foo & Bar` +// finally, put them into `MappedType` as the original implementation +type Intersection = MappedType< + UnionToIntersection[number]> +>; +export function IntersectionType(...classRefs: T) { abstract class IntersectionClassType { constructor() { - allClassRefs.forEach((classRef) => { + classRefs.forEach((classRef) => { inheritPropertyInitializers(this, classRef); }); } } - allClassRefs.forEach((classRef) => { + classRefs.forEach((classRef) => { inheritValidationMetadata(classRef, IntersectionClassType); inheritTransformationMetadata(classRef, IntersectionClassType); }); - const intersectedNames = allClassRefs.reduce( - (prev, ref) => prev + ref.name, - '', - ); + const intersectedNames = classRefs.reduce((prev, ref) => prev + ref.name, ''); Object.defineProperty(IntersectionClassType, 'name', { value: `Intersection${intersectedNames}`, }); - return IntersectionClassType as MappedType; + return IntersectionClassType as Intersection; } diff --git a/tests/intersection-type-multiple.helper.spec.ts b/tests/intersection-type-multiple.helper.spec.ts index b8239efd..ecd9b2f1 100644 --- a/tests/intersection-type-multiple.helper.spec.ts +++ b/tests/intersection-type-multiple.helper.spec.ts @@ -31,7 +31,23 @@ describe('IntersectionType', () => { patronymic!: string; } - class UpdateUserDto extends IntersectionType(ClassA, ClassB, ClassC) {} + class ClassD { + @IsString() + alpha = 'defaultStringAlpha'; + } + + class ClassE { + @IsString() + beta = 'defaultStringBeta'; + } + + class UpdateUserDto extends IntersectionType( + ClassA, + ClassB, + ClassC, + ClassD, + ClassE, + ) {} describe('Validation metadata', () => { it('should inherit metadata for all properties from class A and class B', () => { @@ -45,6 +61,8 @@ describe('IntersectionType', () => { 'lastName', 'hash', 'patronymic', + 'alpha', + 'beta', ]); }); describe('when object does not fulfil validation rules', () => { @@ -74,6 +92,8 @@ describe('IntersectionType', () => { updateDto.lastName = 'lastNameTest'; updateDto.login = 'mylogintesttest'; updateDto.patronymic = 'patronymicTest'; + updateDto.alpha = 'alphaTest'; + updateDto.beta = 'betaTest'; const validationErrors = await validate(updateDto); expect(validationErrors.length).toEqual(0); @@ -105,6 +125,8 @@ describe('IntersectionType', () => { expect(updateUserDto.login).toEqual('defaultLoginWithMin10Chars'); expect(updateUserDto.firstName).toEqual('defaultFirst'); expect(updateUserDto.hash).toEqual('defaultHashWithMin5Chars'); + expect(updateUserDto.alpha).toEqual('defaultStringAlpha'); + expect(updateUserDto.beta).toEqual('defaultStringBeta'); }); }); });