Skip to content

Commit

Permalink
fix(): intersect more than 4 classes
Browse files Browse the repository at this point in the history
  • Loading branch information
kpkonghk01 committed Sep 30, 2022
1 parent 774e478 commit e8d2f4a
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 29 deletions.
53 changes: 25 additions & 28 deletions lib/intersection-type.helper.ts
@@ -1,54 +1,51 @@
import { Type } from '@nestjs/common';

import { MappedType } from './mapped-type.interface';
import {
inheritPropertyInitializers,
inheritTransformationMetadata,
inheritValidationMetadata,
} from './type-helpers.utils';

export function IntersectionType<A, B>(
target: Type<A>,
source: Type<B>,
): MappedType<A & B>;

export function IntersectionType<A, B, C>(
target: Type<A>,
sourceB: Type<B>,
sourceC: Type<C>,
): MappedType<A & B & C>;
// https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;

export function IntersectionType<A, B, C, D>(
target: Type<A>,
sourceB: Type<B>,
sourceC: Type<C>,
sourceD: Type<D>,
): MappedType<A & B & C & D>;
// It converts ClassRefs array `Type<Class>[]` to `Class[]` by `infer`
// e.g. `ClassRefsToConstructors<[Type<Foo>, Type<Bar>]>` becomes `[Foo, Bar]`
type ClassRefsToConstructors<T extends Type[]> = {
[U in keyof T]: T[U] extends Type<infer V> ? V : never;
};

export function IntersectionType<A, T extends { new (...arg: any): any }[]>(
classA: Type<A>,
...classRefs: T
): MappedType<A> {
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<T extends Type[]> = MappedType<
UnionToIntersection<ClassRefsToConstructors<T>[number]>
>;

export function IntersectionType<T extends Type[]>(...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<A>;
return IntersectionClassType as Intersection<T>;
}
24 changes: 23 additions & 1 deletion tests/intersection-type-multiple.helper.spec.ts
Expand Up @@ -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', () => {
Expand All @@ -45,6 +61,8 @@ describe('IntersectionType', () => {
'lastName',
'hash',
'patronymic',
'alpha',
'beta',
]);
});
describe('when object does not fulfil validation rules', () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
});
});
});

0 comments on commit e8d2f4a

Please sign in to comment.