JavaScript: Merge multiple classes

Viewed 820

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?

4 Answers

There is a really neat way of doing this using the mixin pattern.

class UserDto {}
interface UserDto extends UserEmail, UserUsername, UserPassword {}
applyMixins(UserDto, [UserEmail, UserUsername, UserPassword]);

const userDto = new UserDto();

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

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

using this util:

function applyMixins(derivedCtor: any, constructors: any[]) {
    constructors.forEach((baseCtor) => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
            Object.defineProperty(
                derivedCtor.prototype,
                name,
                Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
                    Object.create(null)
            );
        });
    });
}

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.

Can this achieve what you need?

Class User{
   userEmail: UserEmail;
   userUserName: UserUserName;
   userPassword: UserPassword;
}

I think your code would make more sense(and solve the problem) this way:

If you really need join three classes, you could try mixins: https://javascript.info/mixins


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

  @IsEmail()
  email!: string;
}

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

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

class User extends UserSecret {
   /// Secret things here...
}

class UserDto extends UserPassword {
   /// Password things here...
}

Related