Search code examples
javascripttypescriptdtomonorepoclass-validator

JavaScript: Merge multiple classes


I want to combine multiple classes for reusability and consistency amongst backend and frontend. Something like:

import {
  IsEmail,
  IsString,
  MaxLength,
  MinLength,
  validateSync
} from "class-validator";

class UserUsername {
  @IsString()
  @MinLength(3)
  @MaxLength(10)
  username!: string;
}

class UserEmail {
  @IsEmail()
  email!: string;
}

class UserPassword {
  @IsString()
  @MinLength(8)
  password!: string;
}

class UserSecret {
  @IsString()
  secret!: string;
}

class User /* extends UserEmail, UserUsername, UserSecret */ {}
class UserDto /* extends UserEmail, UserUsername, UserPassword */ {}

const userDto = new UserDto();

userDto.username = "noerror";
userDto.email = "error";
userDto.password = "error";

console.log(validateSync(userDto).toString());

Is something similar anyway possible?

Note: I do not mean types only like TypeScript's &. The main purpose is to reuse class validation.

Link to sandbox

The Problem:

export class User {
  @IsUUID()
  id!: string;

  @IsNotEmpty()
  @IsString()
  @MinLength(3)
  username!: string;

  @IsEmail()
  email!: string;

  @IsNotEmpty()
  @IsString()
  secret!: string;

  @IsNotEmpty()
  @IsAlpha()
  firstName!: string;

  @IsNotEmpty()
  @IsAlpha()
  lastName!: string;

  @IsBoolean()
  isEmailVerified!: boolean;
}

export class UserDto {
  @IsUUID()
  id!: string;

  @IsNotEmpty()
  @IsString()
  @MinLength(3)
  username!: string;

  @IsEmail()
  email!: string;

  secret?: never;

  @IsNotEmpty()
  @IsAlpha()
  firstName!: string;

  @IsNotEmpty()
  @IsAlpha()
  lastName!: string;

  @IsBoolean()
  isEmailVerified!: boolean;
}

export class SignUpUserDto {
  id?: never;

  @IsNotEmpty()
  @IsString()
  @MinLength(3)
  username!: string;

  @IsEmail()
  email!: string;

  secret?: never;

  @IsNotEmpty()
  @IsAlpha()
  firstName!: string;

  @IsNotEmpty()
  @IsAlpha()
  lastName!: string;

  isEmailVerified?: never;
}

export class UpdateUserDto {
  id?: never;

  @IsOptional()
  @IsNotEmpty()
  @IsString()
  @MinLength(3)
  username?: string;

  @IsOptional()
  @IsEmail()
  email?: string;

  secret?: never;

  @IsOptional()
  @IsNotEmpty()
  @IsAlpha()
  firstName?: string;

  @IsOptional()
  @IsNotEmpty()
  @IsAlpha()
  lastName?: string;

  isEmailVerified?: never;
}

export class SignInUserDto {
  @IsString()
  @IsNotEmpty()
  username!: string;

  @IsNotEmpty()
  @IsString()
  password!: string;
}

See how repetitive the code is?


Solution

  • Thanks to Filip Kaštovský who pointed me to this doc: How Does A Mixin Work?

    I got to solve it this way:

    import { IsString, IsUUID, MinLength, validateSync } from 'class-validator';
    
    type Constructor<T = {}> = new (...args: any[]) => T;
    
    export function WithUserId<TBase extends Constructor>(Base: TBase) {
      class UserId extends Base {
        @IsUUID()
        id!: string;
      }
    
      return UserId;
    }
    
    export function WithUserUsername<TBase extends Constructor>(Base: TBase) {
      class UserUsername extends Base {
        @IsString()
        @MinLength(3)
        username!: string;
      }
    
      return UserUsername;
    }
    
    export function WithUserSecret<TBase extends Constructor>(Base: TBase) {
      class UserSecret extends Base {
        @IsString()
        secret!: string;
      }
    
      return UserSecret;
    }
    
    class User extends WithUserId(WithUserUsername(WithUserSecret(class {}))) {}
    
    const user = new User();
    
    user.id = "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d";
    user.username = "a";
    user.secret = "a";
    
    console.log(validateSync(user).toString());
    

    Link to sandbox

    EDIT: I cloned @nestjs/mapped-types and modified it to work in both the browser and nodejs. I wanted to publish it but I do not know a lot about publishing and whether it is actually going to work. It works fine in my set-up with Nx because I used Nx to create the library.