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.
- decorator | 데코레이터는 결국, 특정 무언가(ex. 클래스)에 적용되는 함수이다.
- 데코레이터 함수의 첫글자는 대문자로 하는 것이 컨벤션이다.
- 이 함수를 직접 호출하는 것이 아니라, 특정 무언가에 decorator로 추가한다.
- decorator arguments | 데코레이터 함수는 매개변수에서 자기 자신이 어디에서 사용되는지를 받는다.
- 예제에서 Logger는 클래스의 데코레이터이고, 클래스는 생성자 함수의 syntatic sugar이다. => target의 타입을 Funtion으로 전달해 준다.
- 콘솔에 작성되는 순서를 살펴보면, 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가 실행된다.
- _ | 함수 매개변수에서 under score 기호는, 필요하지만 함수 내에서는 사용되지 않는 인자를 가리킨다.
- Person 클래스의 생성자 함수가 실행되면, ShowElement 함수가 실행된다. => DOM 요소에 h1 태그가 등록된다.
- 여러 개의 데코레이터를 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);
}
}
- 프로퍼티로 데코레이터를 사용할 때, 데코레이터 함수는 2개의 인수를 받을 수 있다. => target, propertyName
- 클래스의 생성자 함수(new)를 이용해 인스턴스를 생성하지 않더라도, 데코레이터가 실행된다.
- 데코레이터 함수를 객체의 프로퍼티 중 하나로 정의했기 때문에, js에 의해 클래스 정의 구문이 분석될 때 함께 실행된다. 즉, 클래스의 정의와 함께 Log가 실행된다.
- 여러 개의 instance를 생성할 때 마다 Log가 실행되는 것이 아니다.
- ??? constructor의 매개변수를 축약형으로 작성하면, 데코레이터 함수 부분에서 에러가 발생한다(decorators not valid here).
- target | object의 프로토타입이 출력된다.
- 메서드인 constructor(class Product), set productPrice, getPriceWithTax만 콘솔에 출력되고, name과 price와 같은 state 값은 출력되지 않는다.
- name과 price는 생성되는 인스턴스에 따라 달라지는 값이기 때문에, 프로토타입에 포함되지 않는다.
- 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);
}
}
- new 키워드를 이용해 instance를 생성하지 않더라도 decorator가 실행된다.
- target | decorator의 타겟으로, Log와 Log2의 타겟은 모두 class Product의 프로토타입이다.
- propertyName | set productPrice, getPriceWithTax
- descriptor | configurable, enumerable(열거) 프로퍼티와 get, set 메서드 정보가 저장된 객체를 반환한다.
- Object.defineProperty(obj, prop, descriptor)
- js의 defineProperty는 어떤 객체의 프로퍼티에 대한 더 자세한 정보를 제공해 준다.
- Object.defineProperty(obj, prop, descriptor)
- position | parameter decorator를 등록한 매개변수의 위치(0부터 시작)를 반환한다.
- 데코레이터 실행 순서
- 데코레이터가 작성된 순서대로 실행된다.
- 단, 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()
- targetConstructor의 타입을 Function으로 지정하면 new targetConstructor()에서 에러가 발생한다.
- Function 타입 !== 생성자 함수 타입 이기 때문이다.
- => type Function has no construct signature.
- 1번 예제에서 Example 클래스가 정의될 때 factory에 의해 생성된 데코레이터 함수가 실행되기 때문에 instance를 생성하지 않더라도 "harold"가 DOM에 출력된다.
- 2번 예제에서도 마찬가지로, Example 클래스가 정의될 때 factory에 의해 생성된 데코레이터 함수가 실행된다. 이 때에는 데코레이터 함수가 어떤 일을 하는 것이 아니라 확장된 클래스를 반환하기만 한다.
- ??? showElement2의 반환 타입을 any로 지정하지 않으면 에러가 발생한다. => 예제에서 데코레이터 함수의 반환 타입은 void 또는 Example이어야 하는데, 무명 클래스를 반환하고 있기 때문에 에러가 발생한다.
- 타입스크립트에 의해, 확장된 클래스는 기존의 클래스를 대체한다. => 해당 클래스가 정의될 때에는 실행되지 않고, 인스턴스를 생성할 때(new Example())에만 실행된다.그러나 반드시 대체해야 할 필요는 없다.
- 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 포스팅 참조)
- arrow function이나 bind 메서드를 사용하지 않으면 undefined이 출력되는 것을 확인할 수 있다.
- 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!
- 타입스크립트에 의해, newDescriptor가 기존의 descriptor를 대체한다.
- 기존 descriptor의 get 메서드는 originalMethod인 descriptor.value를 읽는 getter이다.
- 새로운 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)
- submit 이벤트가 발생하면 사용자에게 입력 받은 값으로 새로운 Course를 만든다.
- 이 때 유효성 검사(validation check) 로직을 데코레이터로 작성할 수 있다.
- 유효성 검사가 필요한 프로퍼티에 데코레이터를 각각 추가할 수 있다. 이때 데코레이터를 직접 작성할 수도 있지만, 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 |