@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를 한번 훑어보자
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>>;
}
아래와 같이 구현되어 있는데 이 함수가 상당히 재미있었다.
해당 함수의 매개변수로 아래 두개는 받는다.
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>>;
다른 개발자가 짠 코드를 훑어보는 것에 요즘 재미가 들었다.
멋진 아이디어와 좋은 방법들, 배워야하는 점이 상당히 많은 것 같다. 😎