본문 바로가기

Study/TypeScript

Decorators

what is decorators? 

// tsconfig.json
"compilerOptions": {
  "experimentalDecorators": true,
  // ...
}

 

  • decorator를 사용하려면, tsconfig에서 위의 옵션을 true로 설정해 주어야 한다. 

 

function Logger(target: Function) {
  console.log("decorator: logging...");
  console.log("Logger target: ", target); 
}

@Logger // decorator! 
class Person {
  name = "Harry";
  
  constructor() {
    console.log("creating new obj by Person class...")
  }
}

const test = new Person();
console.log(test);

/*
  decorator: logging...
  Logger target: ...
  creating new obj by Person class...
  Person { name: 'Harry' }
*/

 

A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function(expression은 함수로 평가되어야 한다) that will be called at runtime with information about the decorated declaration.

 

  1. decorator | 데코레이터는 결국, 특정 무언가(ex. 클래스)에 적용되는 함수이다. 
    • 데코레이터 함수의 첫글자는 대문자로 하는 것이 컨벤션이다. 
    • 이 함수를 직접 호출하는 것이 아니라, 특정 무언가에 decorator로 추가한다. 
  2. decorator arguments | 데코레이터 함수는 매개변수에서 자기 자신이 어디에서 사용되는지를 받는다. 
    • 예제에서 Logger는 클래스의 데코레이터이고, 클래스는 생성자 함수의 syntatic sugar이다. => target의 타입을  Funtion으로 전달해 준다. 
  3. 콘솔에 작성되는 순서를 살펴보면, decorator가 실행된 후에 생성자 함수(new Person())가 실행되는 것을 알 수 있다. 
    • decorator는 대상 클래스가 인스턴스화(instantiate) 되지 않았을 때에도, 즉 클래스가 정의되기만 해도 실행된다. 

decorator factory

 decorator 함수를 반환하는 팩토리 함수를 정의해서, decorator 함수를 사용(apply)하는 시점에 구성할 수 있다. 

 

function Logger(something: string) {
  return function(target: Function) {
    console.log(something);
    console.log(target);
  } 
}

@Logger("also decorator")
class Person {
  name = "Harry";
  
  constructor() {
    console.log("creating new obj by Person class...")
  }
}

advanced decorator & meta programming

<meta programming>JavaScript / TypeScript meetaprogramming. Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running.

 

function ShowElement(tag: string, id:string) {
  return function(_: Function) {
    const element = document.querySelector(`#${id}`);
    if(element) {
      element.innerHtml = tag; 
    }
  }
}

/*
function ShowElement(id: string) {
    return function (constructor: any) {
        const targetElement = document.querySelector(`#${id}`) as HTMLElement;
        const target = new constructor('harold'); // here 
        targetElement.innerHTML = target.name; // harold
    }
}
*/

@ShowElement('<h1>hello</h1>', 'app');
class Person {
  constructor(name) {
    this.name = name; 
    console.log("creating new obj by Person class...")
  }
}

const test = new Person(); // 생성자 함수를 호출하기 전, decorator가 실행된다.

 

  1. _ | 함수 매개변수에서 under score 기호는, 필요하지만 함수 내에서는 사용되지 않는 인자를 가리킨다. 
  2.  Person 클래스의 생성자 함수가 실행되면, ShowElement 함수가 실행된다. => DOM 요소에 h1 태그가 등록된다. 
  3. 여러 개의 데코레이터를 apply 할 수도 있다. 
    • 이 때 데코레이터 함수는 나중에 작성한 것부터 실행된다(아래에서 위로). 단, 데코레이터 팩토리를 사용하고 있다면, 팩토리 함수가 실행되고 나서 각 데코레이터 함수가 역순으로 실행된다. 
    • 예를 들어 @a() => @b() 순서대로 작성되었다면, a 팩토리 함수 => b 팩토리 함수 => b 데코레이터 함수 => a 데코레이터 함수 순으로 실행된다.  

decorator to property

데코레이터는 생성자 함수가 호출될 때 뿐만 아니라, 어디에든 적용할 수 있다. 예를 들어 어떤 객체의 프로퍼티로 데코레이터 함수를 사용할 수 있다. 

 

// target: object? 
function Log(target: any, propertyName: string | Symbol) {
    console.log('this is property decorator');
    console.log(target, propertyName)
}

class Product {
    @Log // here! 
    name: string; 
    private price: number; 
        
    constructor(name: string, price: number) {
        this.name = name;
        this.price= price
     }

    set productPrice(value: number) {
        if (value <= 0) return;
        this.price = value;
    }
    getPriceWithTax(taxRatio: number) {
        if (taxRatio < 0) return;
        return this.price * (1 + taxRatio);
    }
}

 

  1. 프로퍼티로 데코레이터를 사용할 때, 데코레이터 함수는 2개의 인수를 받을 수 있다. => target, propertyName
  2. 클래스의 생성자 함수(new)를 이용해 인스턴스를 생성하지 않더라도, 데코레이터가 실행된다. 
    • 데코레이터 함수를 객체의 프로퍼티 중 하나로 정의했기 때문에, js에 의해 클래스 정의 구문이 분석될 때 함께 실행된다. 즉, 클래스의 정의와 함께 Log가 실행된다. 
    • 여러 개의 instance를 생성할 때 마다 Log가 실행되는 것이 아니다. 
  3. ??? constructor의 매개변수를 축약형으로 작성하면, 데코레이터 함수 부분에서 에러가 발생한다(decorators not valid here).
  4. target | object의 프로토타입이 출력된다.
    • 메서드인 constructor(class Product), set productPrice, getPriceWithTax만 콘솔에 출력되고, name과 price와 같은 state 값은 출력되지 않는다. 
    • name과 price는 생성되는 인스턴스에 따라 달라지는 값이기 때문에, 프로토타입에 포함되지 않는다. 
  5. propertyName | 작업하고 있는 프로퍼티의 이름이 출력된다. 예제에서는 'name'이 출력된다.
    • 만약 프로퍼티를 작성할 때  private price:number; => name:string 순으로 작성했다면 'price'가 출력된다. 

decorator to accessor & parameter

<accessor properties> In Javascript, accessosr properties are methods that get or set the value of an object. For that, we use these two keywords: get - to define a getter method to get the property value. set - to define a setter method to set the property value.

 

// 1. method decorator
// PropertyDescriptor : ts 제공 내장 타입
function Log(target: any, propertyName: string | Symbol, descriptor: PropertyDescriptor) {
    console.log('this is method decorator');
    console.log(target, propertyName, descriptor);
}

// 2. parameter decorator
// position은 매개변수에 전달된 인수 중 데코레이터가 실행될 위치를 말한다
function Log2(target: any, propertyName: string | Symbol, position: number) {
    console.log('this is params decorator');
    console.log(target, propertyName, position);
}

class Product {
    name: string; 
    private price: number; 
        
    constructor(name: string, price: number) {
        this.name = name;
        this.price= price
     }
     
    @Log // accessor decorator! 
    set productPrice(value: number) {
        if (value <= 0) return;
        this.price = value;
    }
    
    @Log // method decorator 
    getPriceWithTax(@Log2 taxRatio: number) { 
        if (taxRatio < 0) return;
        return this.price * (1 + taxRatio);
    }
}

 

  1. new 키워드를 이용해 instance를 생성하지 않더라도 decorator가 실행된다.
  2. target | decorator의 타겟으로, Log와 Log2의 타겟은 모두 class Product의 프로토타입이다. 
  3. propertyName | set productPrice, getPriceWithTax
  4. descriptor | configurable, enumerable(열거) 프로퍼티와 get, set 메서드 정보가 저장된 객체를 반환한다. 
    •  Object.defineProperty(obj, prop, descriptor) 
    • js의 defineProperty는 어떤 객체의 프로퍼티에 대한 더 자세한 정보를 제공해 준다. 
  5. position | parameter decorator를 등록한 매개변수의 위치(0부터 시작)를 반환한다.
  6. 데코레이터 실행 순서 
    • 데코레이터가 작성된 순서대로 실행된다. 
    • 단, getPriceWithTax에서는 params 데코레이터가 먼저 실행되고, method에 등록된 데코레이터가 나중에 실행된다. 

decorator returning something

class

클래스가 정의될 때에는 실행되지 않고, 인스턴스를 생성할 때에 실행되는 로직을 추가할 수 있다.

 

// 1. 
function ShowElement(element: string, id: string) {
    return function (targetConstructor: any) {
        const targetElement = document.querySelector(`#${id}`);
        const target = new targetConstructor();
        if (targetElement) {
            targetElement.innerHTML = element; //<h1></h1>
            targetElement.querySelector('h1')!.textContent = target.name; //name is always harold
        }
    }
}

@ShowElement('<h1></h1>', 'app')
class Example {
    name = 'harold'
    constructor() {
        console.log('this is example')
    }
}

// 2. 
function ShowElement2(element: string, id: string): any {
    return function (targetConstructor: any) {
        return class extends targetConstructor {
            constructor() {
                super();
                const targetElement = document.querySelector(`#${id}`);
                if (targetElement) {
                    targetElement.innerHTML = element;
                    targetElement.querySelector('h1')!.textContent = this.name; // here!
                }
            }
        }
    }
}

@ShowElement2('<h1></h1>', 'app')
class Example {
    name = 'harold'
    constructor() {
        console.log('this is example')
    }
}

new Example();

// 3. generic 
function ShowElement2<T extends { new(...args: any[]): { name: string } }>(element: string, id: string) {
    return function (targetConstructor: T) {
        return class extends targetConstructor {
            constructor(...args: any[]) { // here! 
                super();
                const targetElement = document.querySelector(`#${id}`);
                if (targetElement) {
                    targetElement.innerHTML = element;
                    targetElement.querySelector('h1')!.textContent = this.name;
                }
            }
        }
    }
}

@ShowElement2('<h1></h1>', 'app')
class Example {
    name = 'harold'
    constructor() {
        console.log('this is example')
    }
}

new Example()

 

  1. targetConstructor의 타입을 Function으로 지정하면 new targetConstructor()에서 에러가 발생한다.
    •  Function 타입 !== 생성자 함수 타입 이기 때문이다. 
    • => type Function has no construct signature. 
  2. 1번 예제에서 Example 클래스가 정의될 때 factory에 의해 생성된 데코레이터 함수가 실행되기 때문에 instance를 생성하지 않더라도 "harold"가 DOM에 출력된다. 
  3. 2번 예제에서도 마찬가지로, Example 클래스가 정의될 때 factory에 의해 생성된 데코레이터 함수가 실행된다. 이 때에는 데코레이터 함수가 어떤 일을 하는 것이 아니라 확장된 클래스를 반환하기만 한다. 
    • ??? showElement2의 반환 타입을 any로 지정하지 않으면 에러가 발생한다. => 예제에서 데코레이터 함수의 반환 타입은 void 또는 Example이어야 하는데, 무명 클래스를 반환하고 있기 때문에 에러가 발생한다. 
    • 타입스크립트에 의해, 확장된 클래스는 기존의 클래스를 대체한다. => 해당 클래스가 정의될 때에는 실행되지 않고, 인스턴스를 생성할 때(new Example())에만 실행된다.그러나 반드시 대체해야 할 필요는 없다. 
  4. T extends { new(...args: any[]): { name: string }
    • showElement2는 T를 반환한다. 
    • T는 객체인데, 생성자 함수(new)의 실행 결과는 name 프로퍼티를 가지고 있는 객체를 확장한다. 

accessors or methods

접근자와 메서드에서 실행되는 데코레이터 함수는 새로운 descriptor를 반환할 수도 있다. 

 

function Log(
  target: any, 
  propertyName: string | Symbol, 
  descriptor: PropertyDescriptor
): PropertyDescriptor {
    console.log('this is method decorator');
    console.log(target, propertyName, descriptor);
    return { configurable: true, get: ... }; 
}

decorator example

auto binding

class Printer {
    message = 'you clicked me';

    print() {
        console.log(this.message);
    }
}

const test = new Printer();

const btn = document.querySelector('button')!;

btn.addEventListener('click', () => test.print()); // you clicked me
btn.addEventListener('click', () => test.print.bind(testPrinter)); // you clicked me
btn.addEventListener('click', test.print)  // undefined(undefined이 출력되는 이유는 this 포스팅 참조)

 

  1. arrow function이나 bind 메서드를 사용하지 않으면 undefined이 출력되는 것을 확인할 수 있다.
  2. print 함수에 auto bind를 대신 해주는 데코레이터를 추가하면, 화살표 함수나, bind 메서드를 사용하지 않고도 원하는 대로 동작하게 만들 수 있다. 

 

function AutoBind(target: any, methodName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const newDescriptor: PropertyDescriptor = {
        // configurable: true,
        // enumerable: false, // unable to use for-in loop 
        get() {
            console.log(descriptor); 
            const boundFn = originalMethod.bind(this);
            return boundFn;
        }
    };
    return newDescriptor;
}

class Printer {
    message = 'you clicked me';
    @AutoBind
    print() {
        console.log(this.message);
    }
}

const testPrinter = new Printer();

const btn = document.querySelector('button')!;
btn.addEventListener('click', testPrinter.print) // you clicked me!

 

  1. 타입스크립트에 의해, newDescriptor가 기존의 descriptor를 대체한다. 
  2. 기존 descriptor의 get 메서드는 originalMethod인 descriptor.value를 읽는 getter이다.
  3. 새로운 descriptor에서는 getter가 기존 method에 bind 함수를 실행한 값을 반환하게 함으로써, autobind 데코레이터를 만들 수 있다. 

validation check

class Course {
  title: string;
  price: number;
  
  constructor(title: string, price: number) {
    this.title = title; 
    this.price = price; 
  }
}

const onSubmit = (event: SubmitEvent) => {
  event.preventDefault();
  
  const titleInput = document.querySelector('#title') as HTMLInputElement; 
  const priceInput = document.querySelector('#price') as HTMLInputElement;
  new Course(titleInput.value, +priceInput.value); 
}

document.querySelector('form').addEventListener('submit', onSubmit)

 

  1. submit 이벤트가 발생하면 사용자에게 입력 받은 값으로 새로운 Course를 만든다.
  2. 이 때 유효성 검사(validation check) 로직을 데코레이터로 작성할 수 있다. 
  3. 유효성 검사가 필요한 프로퍼티에 데코레이터를 각각 추가할 수 있다. 이때 데코레이터를 직접 작성할 수도 있지만, third party library에서 받아 이용할 수도 있다. 

 

interface validateConfiguration {
  [targetKey: string] : {
    [key: string] : string[]; // ['required', 'positive']
  }
}

const validators: validateConfiguration = {}; // no validator yet
/* 
validators = { Course: {
  title: ['required'], price: ['positive'] 
} }
*/ 

function GetIsRequired(target: any, propertyName: string) {
  const validateTarget = target.constructor.name; // Course
  
  validators[validateTarget] = {
    ...validators[validateTarget],
    [propertyName]: ['required']
    // [propertyName]: [...(validators[validateTarget]?.[propName] ?? []), 'required']
  }
}

function GetIsPositive(target: any, propertyName: string) {
  const validateTarget = target.constructor.name; 
  
  validators[validateTarget] = {
    ...validators[validateTarget],
    [propertyName]: ['positive'] 
  }
}

function validate(object: object) {
  const validateTarget = target.constructor.name; 
  if(!validators[validateTarget]) return; 

  let isValid = true; 
  for (const targetKey in validators[validateTarget]) {
    for (const validator of validators[validateTarget][targetKey]) {
      switch (validator) {
        case 'required':
          isValid = isValid && !!object[targetKey];
          break;
        case 'positive':
          isValid = isValid && object[targetKey] > 0;
          break;
      }
    }
  }
  return isValid;
}

class Course {
  @GetIsNull
  title: string;
  @GetIsPositive
  price: number;
  
  constructor(title: string, price: number) {
    this.title = title; 
    this.price = price; 
  }
}

//...
const onSubmit = (event: SubmitEvent) => {
  event.preventDefault();
  // ...
  const newCourse = new Course(titleInput.value, +priceInput.value); 
  if(!validate(newCourse)) return; 
  console.log(newCourse); 
}

 

  • 처음에는 validators가 텅 빈 객체이지만, Course class가 선언됨에 따라 프로퍼티 데코레이터가 실행된다. 

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

Webpack  (0) 2022.08.10
Module  (0) 2022.08.10
Generic  (0) 2022.07.24
Advanced Types  (0) 2022.07.23
Interface  (0) 2022.07.21