Search code examples
typescriptoopnestjsclass-validator

class-validator fusioning multiple dto in a convenient way for validation in typescript


I was doing my nestjs project and i was finding the class-validation very annoying. I was repeating the same classes over and over again with the same decorators.

ex:

export class DTO1 {
    @IsDefined()
    @IsString()
    name: string;

    @IsDefined()
    @Type(() => Number)
    @IsInt()
    client: number;

    @IsDefined()
    @IsIn(['value1', 'value2'])
    role: 'value1' | 'value2';
}

export class DTO2 {
    @IsDefined()
    @IsNotEmpty()
    @IsString()
    name: string;

    @IsDefined()
    @IsNotEmpty()
    @Type(() => Number)
    @IsInt()
    team: number;
}

export class DTO3 {
    @IsDefined()
    @IsNotEmpty()
    @Type(() => Number)
    @IsInt()
    team: number;

    @IsDefined()
    @IsNotEmpty()
    @Type(() => Number)
    @IsInt()
    client: number;
}

export class DTO4 {
    @IsDefined()
    @IsNotEmpty()
    @IsIn(['value1', 'value2', 'value3', 'value4'])
    mode: 'value1' | 'value2' | 'value3' | 'value4';

    @IsDefined()
    @IsNotEmpty()
    @Type(() => Number)
    @IsInt()
    team: number;
}

A lot of repetition because you have alot of query that are the same from dto to dto but do not appear consistantly enough for you make a base class then extending it

export class Base {
    @IsDefined()
    @IsString()
    elemEveruoneHas: string;
}

export class DTO1 extends Base {
    @IsDefined()
    @IsNotEmpty()
    @Type(() => Number)
    @IsInt()
    id: number;
}

what i wanted was to create a few classes for reacuring elements then have a way to generate them into the specific DTO.

it would look like :

export class DTO_name {
    @IsDefined()
    @IsNotEmpty()
    @IsString()
    name: string;
}

export class DTO_team {
    @IsDefined()
    @IsNotEmpty()
    @Type(() => Number)
    @IsInt()
    team: number;
}

export class DTO_client {
    @IsDefined()
    @IsNotEmpty()
    @Type(() => Number)
    @IsInt()
    client: number;
}

export class DTO1 extends DTO(DTO_name, DTO_client) {
    @IsDefined()
    @IsIn(['value1', 'value2'])
    role: 'value1' | 'value2';
}
export class DTO2 extends DTO(DTO_name, DTO_team) {}
export class DTO3 extends DTO(DTO_team, DTO_client) {}
export class DTO4 extends DTO(DTO_team) {
    @IsDefined()
    @IsNotEmpty()
    @IsIn(['value1', 'value2', 'value3', 'value4'])
    mode: 'value1' | 'value2' | 'value3' | 'value4';
}

Right now i have something like this which is better but ugly :

type Constructor<T = {}> = new (...args: any[]) => T;

export const DTO_name = <TBase extends Constructor>(Base: TBase = class {} as TBase) => {
    class _ extends Base {
        @IsDefined()
        @IsNotEmpty()
        @IsString()
        name: string;
    }

    return _;
}

export const DTO_team = <TBase extends Constructor>(Base: TBase = class {} as TBase) => {
    class _ extends Base {
        @IsDefined()
        @IsNotEmpty()
        @Type(() => Number)
        @IsInt()
        team: number;
    }

    return _;
}

export const DTO_client = <TBase extends Constructor>(Base: TBase = class {} as TBase) => {
    class _ extends Base {
        @IsDefined()
        @IsNotEmpty()
        @Type(() => Number)
        @IsInt()
        client: number;
    }

    return _;
}

export class DTO1 extends DTO_name(DTO_client()) {
    @IsDefined()
    @IsIn(['value1', 'value2'])
    role: 'value1' | 'value2';
}
export class DTO2 extends DTO_name(DTO_team()) {}
export class DTO3 extends DTO_team(DTO_client()) {}
export class DTO4 extends DTO_team() {
    @IsDefined()
    @IsNotEmpty()
    @IsIn(['value1', 'value2', 'value3', 'value4'])
    mode: 'value1' | 'value2' | 'value3' | 'value4';
}

Does any know how i could improve this so that i dont have to recreate the entire setup for each element class and maybe if we could have have a function that generate the inherited class that does the chaining inheritance so we could have the DTO(Test1, Test2, Test3) ?


Solution

  • Not in the exact form you were wanting, but you could achieve this with Nest's @nestjs/mapped-types's IntersectionType, doing something like export class DTO extends IntersectionType(Class1, IntersectionType(Class2, Class3)) {} (and of course nesting as necessary)