본문 바로가기
정리/TypeScript

Type에서 데코레이터 사용하기

by 난파선 2022. 11. 5.

데코레이터

TypeScript 및 ES6에 클래스가 도입됨에 따라, 클래스 및 클래스 멤버에 어노테이션을 달거나 수정하기 위해 추가 기능이 필요한 특정 시나리오가 있습니다. 데코레이터는 클래스 선언과 멤버에 어노테이션과 메타-프로그래밍 구문을 추가할 수 있는 방법을 제공합니다. 데코레이터는 JavaScript에 대한 2단계 제안이며 TypeScript의 실험적 기능으로 이용 가능합니다.

설정방법

명령줄

tsc --target ES5 --experimentalDecorators

tsconfig.json

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

 

데코레이터 합성

function first() {  // 데코레이터 팩토리(Decorator Factories)
    console.log("first(): factory evaluated");
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("first(): called");
    };
}

function second() {
    console.log("second(): factory evaluated");
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("second(): called");
    };
}

class ExampleClass {
    @first()
    @second()
    method() {
        console.log("method(): called");
    }
}

/**
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
*/

데코레이터 함수 실행 후에 하나씩 리턴되는 함수 실행

 

데코레이터 선언부 코드

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

 

 

클래스 데코레이터(Class Decorator)

1번째 예제

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class BugReport {
    type = "report";
    title: string;

    constructor(t: string) {
        this.title = t;
    }
}

let bugReport = new BugReport("test");
console.log(`bugReport = ${JSON.stringify(bugReport)}`);

/*
bugReport = {"type":"report","title":"test"}
*/

생성자를 수정 불가능하게 설정하고 있다.

2번째 예제

function ClassDecorator(constructor: Function) {
    constructor.prototype.test = function () {
        console.log("test");
    }
}

@ClassDecorator
class Test {
    test() {}
}
/**
test
*/

function ClassDecorator2<T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
        constructor(...args: any[]) {
            super(args);
        }

        public test() {
            console.log("test2");
        }
    };
}

@ClassDecorator2
class Test {
    test() {}
}

let test = new Test();
test.test();

/**
test2
*/
  • 프로토타입으로 프로퍼티, 메서드 구현 가능하다.
  • 제너릭 <T extends { new (...args: any[]): {} }>를 사용해서 클래스 구현부를 리턴 가능하다.
  • <T extends { new (...args: any[]): {} }> : any[] 배열의 매개변수를 가지는 생성자가 포함된 클래스

 

 

메서드 데코레이터(Method Decorator)

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        /**
         * 1. 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입입니다.
         * 2. 멤버의 이름
         * 3. 멤버의 프로퍼티 설명자
         * */
        console.log(`target = ${JSON.stringify(target)}`);
        console.log("propertyKey = " + propertyKey);
        console.log(`descriptro = ${JSON.stringify(descriptor)}`);
        descriptor.enumerable = value;
    };
}

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

/**
target = {}
propertyKey = greet
descriptro = {"writable":true,"enumerable":false,"configurable":true}
*/

descriptor

  • configurable : 속성의 값을 변경할 수 있고, 객체에서 삭제할 수도 있으면 true입니다. 기본 값은 false입니다.
  • enumerable : 속성이 객체의 속성 열거 시 노출되면 true입니다. 기본 값은 false입니다. 데이터 서술자는 다음의 키를 선택적으로 포함할 수 있습니다.
  • value : 속성에 연관된 값입니다. 유효한 JavaScript 값(숫자, 객체, 함수 등)은 모두 사용할 수 있습니다. 기본 값은 undefined입니다.
  • writable : 할당 연산자 (en-US)로 속성의 값을 바꿀 수 있으면 true입니다. 기본 값은 false입니다.

 

 

프로퍼티 데코레이터(Property Decorator)

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

function property(target: any, propertyKey: string) {   // 기본형
    console.log(`target = ${JSON.stringify(target)}`);
    console.log(`propertyKey = ${propertyKey}`);
}

class Greeter {
    @format("Hello, %s")
    greeting: string;

    @property
    sss: string;

    constructor(message: string) {
        this.greeting = message;
        this.sss = "sss";
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    }
}
// 실행
let greeter = new Greeter("world");
console.log(greeter.greet());

/**
target = {}
propertyKey = sss
Hello, world
*/

greeting 매개변수에 키워드 format으로 "Hello, %s" 메타데이터를 설정한다.
getFormat 함수로 설정한 메타데이터 값을 가져온다.
가져온 데이터에서 "%s" 문자열을 변환한다.

 

 

매개변수 데코레이터(Parameter Decorator)

import "reflect-metadata";
const requiredMetadataKey = Symbol("required");

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    console.log(`target = ${JSON.stringify(target)}`);
    console.log(`propertyKey = ${String(propertyKey)}`);
    console.log(`parameterIndex = ${parameterIndex}`);

    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}


class BugReport {
    type = "report";
    title: string;

    constructor(t: string) {
        this.title = t;
    }

    print(@required verbose: boolean) {
        if (verbose) {
            return this.title;
        } else {
            return `type: ${this.type}\ntitle: ${this.title}`;
        }
    }
}

let bugReport = new BugReport("test");
console.log(`bugReport.print(true) = ${bugReport.print(true)}`);
console.log(`bugReport.print(false) = ${bugReport.print(false)}`);

/**
target = {}
propertyKey = print
parameterIndex = 0
bugReport.print(true) = test
bugReport.print(false) = type: report
title: test
*/

참고자료

'정리 > TypeScript' 카테고리의 다른 글

reflect-metadata  (0) 2022.11.08
TypeORM에서 메모리DB(pg-mem) 사용해서 테스트 코드 만들기  (0) 2022.11.04

댓글