PartialType in @nestjs/mapped-types

김동현·2024년 1월 31일
0

Nestjs

목록 보기
4/6
post-thumbnail

@nestjs/mapped-types

@nestjs/mapped-types 는 중복되는 DTO 프로퍼티를 줄이고 재사용할 수 있게 도움으로서 더 효율적으로 개발할 수 있게 도와주는 라이브러리이다.

예를들면 PartialType이 있는데 아래와 같이 사용할 수 있다.

import { IsNumber, IsOptional, IsString } from 'class-validator';

export class CreateMovieDto {
  @IsString()
  readonly title: string;

  @IsNumber()
  readonly year: number;

  @IsOptional()
  @IsString({ each: true })
  readonly genres: string[];
}

위와 같은 Movie 생성 DTO가 있을 때 보통 Update DTO는 존재하는 값들의 Optional일 때가 많다. 즉, 아래와 같은 코드가 된다

import { IsNumber, IsOptional, IsString } from 'class-validator';

export class UpdateMovieDto {
  @IsString()
  @IsOptional()
  readonly title: string;

  @IsNumber()
  @IsOptional()
  readonly year: number;

  @IsOptional()
  @IsString({ each: true })
  readonly genres: string[];
}

이러한 중복된 코드를 줄이고 더 효율적으로 개발하기 위한 라이브러리를 제공해준다.
위 코드는 아래의 코드와 같다.

export class UpdateMovieDto extends PartialType(CreateMovieDto) {}

우리가 DTO를 생성할 때 존재하는 DTO에서 특정 프로퍼티를 제거하거나, 모두 Optional화 하거나, 특정 프로퍼티만 선택하고 싶은 경우가 많다.
@nestjs/mapped-types 는 이럴 때 조금 더 효율적으로 개발할 수 있도록 도와주는 라이브러리이다.

✅ PartialType이 어떻게 구현되어 있는지 github를 한번 훑어보자

PartialType

import { Type } from '@nestjs/common';
import { MappedType } from './mapped-type.interface';
import {
  applyIsOptionalDecorator,
  inheritPropertyInitializers,
  inheritTransformationMetadata,
  inheritValidationMetadata,
} from './type-helpers.utils';
import { RemoveFieldsWithType } from './types/remove-fields-with-type.type';

export function PartialType<T>(classRef: Type<T>) {
  // 1. PartialClassType 추상 클래스 생성
  abstract class PartialClassType {
    constructor() {
      // 전달받은 매개변수 class의 프로퍼티를 해당 추상 클래스에 상속하는 함수
      // 해당 함수를 지나면 매개변수 클래스의 프로퍼티가 해당 추상 클래스에 복제된다.
      inheritPropertyInitializers(this, classRef);
    }
  }

  // 매개 변수의 class-validator 메타데이터 상속(추가)해주기
  const propertyKeys = inheritValidationMetadata(classRef, PartialClassType);
  // 매개 변수의  class-transformer 메타데이터 상속(추가)해주기
  inheritTransformationMetadata(classRef, PartialClassType);

  if (propertyKeys) {
    propertyKeys.forEach((key) => {
      // 복사 되어 존재하는 모드 프로퍼티에 IsOptional class-validator 추가해주기
      applyIsOptionalDecorator(PartialClassType, key);
    });
  }

  // 생성된 추상 클래스의 name 프로퍼티 재정의
  Object.defineProperty(PartialClassType, "name", {
    value: `Partial${classRef.name}`,
  });

  return PartialClassType as MappedType<RemoveFieldsWithType<Partial<T>, Function>>;
}

inheritPropertyInitializers

아래와 같이 구현되어 있는데 이 함수가 상당히 재미있었다.
해당 함수의 매개변수로 아래 두개는 받는다.
target -> 생성된 추상클래스
sourceClass -> PartialType에 전달된 Class

이 함수를 지나면 sourceClass의 프로퍼티가 target에 모두 복사(상속) 된다!

// 1. Target Class에 SourceClass의 프로퍼티를 복제한다.
export function inheritPropertyInitializers(
  target: Record<string, any>,
  sourceClass: Type<any>,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  isPropertyInherited = (key: string) => true
) {
  try {
    // SourceClass 생성
    const tempInstance = new sourceClass();
    // 생성된 SourceClass Instance의 프로퍼티 이름 배열 가져오기
    const propertyNames = Object.getOwnPropertyNames(tempInstance);

    propertyNames
      .filter(
        (propertyName) =>
          // 1-1 SourceClass에는 프로퍼티가 존재하고
          typeof tempInstance[propertyName] !== "undefined" &&
          // 1-2 target Class에 프로퍼티가 존재하지 않는 것을 거른다.
          typeof target[propertyName] === "undefined"
      )
    
      // key(propertyName)가 있는 것만 남긴다.
      .filter((propertyName) => isPropertyInherited(propertyName))
      // Target Class 에 남아 있는 SourceClass의 프로퍼티 이름과 값을 패핑한다.
      .forEach((propertyName) => {
        target[propertyName] = tempInstance[propertyName];
      });
  } catch {}
}

이 후 전달받은 매개 변수 클래스의 class-validator, class-transformer 어노테이션을 추가해 준 뒤 @IsOptional() 해주면 SourceClass의 모든 프로퍼티 Optional화 하면 끝!

  // 매개 변수의 class-validator 메타데이터 상속(추가)해주기
  const propertyKeys = inheritValidationMetadata(classRef, PartialClassType);
  // 매개 변수의  class-transformer 메타데이터 상속(추가)해주기
  inheritTransformationMetadata(classRef, PartialClassType);

  if (propertyKeys) {
    propertyKeys.forEach((key) => {
      // 복사 되어 존재하는 모드 프로퍼티에 IsOptional class-validator 추가해주기
      applyIsOptionalDecorator(PartialClassType, key);
    });
  }

  // 생성된 추상 클래스의 name 프로퍼티 재정의
  Object.defineProperty(PartialClassType, "name", {
    value: `Partial${classRef.name}`,
  });

  return PartialClassType as MappedType<RemoveFieldsWithType<Partial<T>, Function>>;

다른 개발자가 짠 코드를 훑어보는 것에 요즘 재미가 들었다.
멋진 아이디어와 좋은 방법들, 배워야하는 점이 상당히 많은 것 같다. 😎

참고 자료

profile
달려보자

0개의 댓글