
"experimentalDecorators": true로 세팅.// 데코레이터는 결국 함수.
function Logger(constructor: Function) {
console.log("Logging...");
console.log(constructor);
}
@Logger
class Person {
name = "Max";
constructor() {
console.log("Creating person obj...");
}
}
const pers = new Person();
console.log(pers);
// Logging...
// class Person {
// constructor() {
// this.name = "Max";
// console.log("Creating person obj...");
// }
// }
// Creating person obj...
// Person {name: 'Max'}
@: 이 기호 바로 뒤에는 반드시 함수를 지정해야한다. 실행하는게 아니라 지정하면 해당 함수가 데코레이터가 된다.
데코레이터는 자바스크립트가 클래스 정의와 생성자 함수 정의를 만난 시점에 실행된다.
// ===== 첫 번째 클래스 데코레이터 =====
// 데코레이터는 결국 함수.
function Logger(logString: string) {
return function (constructor: Function) {
console.log(logString);
console.log(constructor);
};
}
@Logger("LOGGING - PERSON")
class Person {
name = "Max";
constructor() {
console.log("Creating person obj...");
}
}
const pers = new Person();
console.log(pers);
// LOGGING - PERSON
// class Person {
// constructor() {
// this.name = "Max";
// console.log("Creating person obj...");
// }
// }
// Creating person obj...
// Person {name: 'Max'}
function WithTemplate(template: string, hookId: string) {
return function (_: Function) {
// _ : 인자가 들어오는 것을 알지만 필요치 않다.
const hookEl = document.getElementById(hookId);
if (hookEl) {
hookEl.innerHTML = template;
}
};
}
@WithTemplate("<h1>My Person Object</h1>", "app")
class Person {
name = "Max";
constructor() {
console.log("Creating person obj...");
}
}

function WithTemplate(template: string, hookId: string) {
return function (constructor: any) {
const hookEl = document.getElementById(hookId);
const p = new constructor();
if (hookEl) {
hookEl.innerHTML = template;
hookEl.querySelector("h1")!.textContent = p.name;
}
};
}
@WithTemplate("<h1>My Person Object</h1>", "app")
class Person {
name = "Zoe";
constructor() {
console.log("Creating person obj...");
}
}

function Logger(logString: string) {
console.log("LOGGER FACTORY");
return function (constructor: Function) {
console.log(logString);
console.log(constructor);
};
}
function WithTemplate(template: string, hookId: string) {
console.log("TEMPLATE FACTORY");
return function (constructor: any) {
console.log("Rendering Template");
const hookEl = document.getElementById(hookId);
const p = new constructor();
if (hookEl) {
hookEl.innerHTML = template;
hookEl.querySelector("h1")!.textContent = p.name;
}
};
}
@Logger("LOGGING - PERSON")
@WithTemplate("<h1>My Person Object</h1>", "app")
class Person {
name = "Zoe";
constructor() {
console.log("Creating person obj...");
}
}
// LOGGER FACTORY
// TEMPLATE FACTORY
// Rendering Template
// Creating person obj...
// LOGGING - PERSON
// class Person {
// constructor() {
// this.name = "Zoe";
// console.log("Creating person obj...");
// }
// }
데코레이터 실행 순서가 상향식(Bottom-Up)이라는 것을 알 수 있다. 밑에 있는 데코레이터가 먼저 실행되고 위의 것이 나중에 실행되었다.
@가 있어도 해당 부분에서 실행되는 것은 결국 함수니깐!function Log(target: any, propertyName: string | Symbol) {
console.log("Property decorator!");
console.log(target, propertyName);
}
class Product {
@Log
title: string;
private _price: number;
set price(val: number) {
if (val > 0) {
this._price = val;
} else {
throw new Error("옳지 않은 갑 - positive 여야한다.");
}
}
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
getPriceWithTax(tax: number) {
return this._price * (1 + tax);
}
}
// Property decorator!
// {getPriceWithTax: ƒ}
// constructor: class Product
// getPriceWithTax: ƒ getPriceWithTax(tax)
// set price: ƒ price(val)
// [[Prototype]]: Object
// 'title'
title과 _price는 없지만 getPriceWithTax, set와 같은 메서드가 들어있다. 프로토타입에는 메서드를 포함하기 때문.
Product를 인스턴스화하는 곳은 없기 때문에 데코레이터는 자바스크립트에 클래스 정의가 등록되는 시점에 실행된다.
// 만약 이렇게 한다면
class Product {
title: string;
@Log
private _price: number;
}
// 출력되는 프로퍼티 네임은 '_price'다!
function Log2(target: any, name: string, descriptor: PropertyDescriptor) {
console.log("Accessor decorator!");
console.log(target);
console.log(name);
console.log(descriptor);
}
function Log3(
target: any,
name: string | Symbol,
descriptor: PropertyDescriptor
) {
console.log("Method decorator!");
console.log(target);
console.log(name);
console.log(descriptor);
}
function Log4(target: any, name: string | Symbol, position: number) {
console.log("Parameter decorator!");
console.log(target);
console.log(name);
console.log(position);
}
class Product {
@Log
title: string;
private _price: number;
@Log2
set price(val: number) {
if (val > 0) {
this._price = val;
} else {
throw new Error("옳지 않은 갑 - positive 여야한다.");
}
}
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
@Log3
getPriceWithTax(@Log4 tax: number) {
return this._price * (1 + tax);
}
}
// Accessor decorator!
// {getPriceWithTax: ƒ} -> 프로토타입
// price
// {get: undefined, enumerable: false, configurable: true, set: ƒ}
// Parameter decorator!
// {getPriceWithTax: ƒ}
// getPriceWithTax
// 0
// Method decorator!
// {getPriceWithTax: ƒ}
// getPriceWithTax
// {writable: true, enumerable: false, configurable: true, value: ƒ}
Accessor decorator
set price()), 프로퍼티 설명자가 출력되었다.Parameter decorator(매개변수)
// 접근자 데코레이터(Accessor Decorator)
// {get: undefined, enumerable: false, configurable: true, set: ƒ}
// configurable: true
// enumerable: false
// get: undefined
// set: ƒ price(val)
// [[Prototype]]: Object
// 메서드 데코레이터
// {writable: true, enumerable: false, configurable: true, value: ƒ}
// configurable: true
// enumerable: false
// value: ƒ getPriceWithTax(tax)
// writable: true
// [[Prototype]]: Object
undefined, setter는 set price로 작성했으니 정보가 나와있다.데코레이터 자체는 클래스가 정의될 때나 메서드 등이 등록될 때 실행되는 함수일 뿐이다.
function WithTemplate(template: string, hookId: string) {
console.log("TEMPLATE FACTORY");
return function <T extends { new (...args: any[]): { name: string } }>(
originalConstructor: T
) {
return class extends originalConstructor {
constructor(...args: any[]) {
// ...args => ..._도 된다.
super(); // 기존 클래스의 동작 보존
console.log("Rendering Template");
const hookEl = document.getElementById(hookId);
if (hookEl) {
hookEl.innerHTML = template;
hookEl.querySelector("h1")!.textContent = this.name;
}
}
};
};
}
@Logger("LOGGING - PERSON")
@WithTemplate("<h1>My Person Object</h1>", "app")
class Person {
name = "Zoe";
constructor() {
console.log("Creating person obj...");
}
}
const pers = new Person();
console.log(pers);
기존 생성자 함수를 바탕으로 하고있다. 그래서 기존 클래스, 즉 기존 생성자 함수의 모든 프로퍼티가 그대로 보존된다. → 꼭 해야하는 것은 아니다! 해당 프로퍼티를 계속 가져가고 싶어서 extends함.
위처럼 하면 새로운 생성자 함수로 대체하여 기존에 있던 로직 뿐만 아니라 새로운 로직도 함께 수행.
이렇게 하면 실제로 객체의 인스턴스가 생성될 때만 템플릿이 DOM에 렌더링된다. 그러면 클래스가 정의되자마자 데코레이터 함수가 실행되어 템플릿을 렌더링하는 것을 방지할 수 있다.
만약 Person을 인스턴스하지 않았다면(const pers = new Person()을 하지 않았다면) 렌더링이 되지 않는다.
class Product {
@Log
title: string;
private _price: number;
@Log2
set price(val: number) {
if (val > 0) {
this._price = val;
} else {
throw new Error("옳지 않은 갑 - positive 여야한다.");
}
}
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
@Log3
getPriceWithTax(@Log4 tax: number) {
return this._price * (1 + tax);
}
}
desriptor)를 인자로 받는다.Autobind 데코레이터 만들기function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value; // 원본 메서드에 접근
const adjDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
get() {
const boundFn = originalMethod.bind(this); // this는 이 게터 메서드를 실행시킨 대상을 가리킨다.
return boundFn;
},
};
return adjDescriptor;
}
class Printer {
message = "This works!";
@Autobind
showMessage() {
console.log(this.message);
}
}
const p = new Printer();
const button = document.querySelector("button");
button?.addEventListener("click", p.showMessage); // 클릭하여 이벤트 리스너 호출 -> this가 이벤트의 대상을 가리키게 된다.
value라는 프로퍼티가 함수를 가리키고 있다. 이를 이용해서 value로 원본 함수를 가져온다. (const boundFn = originalMethod.bind(this))get : 사용자가 프로퍼티 값에 접근하려 할 때 부가적인 로직을 수행할 것.부가적인 로직 수행 후 원래 함수가 실행되도록 함.
this는 이 게터 메서드를 실행시킨 대상을 가리킨다.
this는 언제나 이 게터를 정의한 객체를 가리킨다.
this를originalMethod에 바인딩함으로써, 원본 메서드의 this 역시 동일한 객체를 가리키게 만들 수 있다.
타입스크립트는 기존 메서드 설명자를 변경하고 기존 메서드 설정값을 새로운 설정값으로 바꿀 것이다. 이때 게터 계층이 추가.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>타입스크립트 이해하기</title>
<script src="07_Decorators/dist/app.js" defer></script>
</head>
<body>
<div id="app"></div>
<button>Click me</button>
<form>
<input type="text" placeholder="Course title" id="title" />
<input type="text" placeholder="Course price" id="price" />
<button type="submit">Save</button>
</form>
</body>
</html>
function Required() {}
function PositiveNumber() {}
function validate(obj: object) {}
class Course {
@Required
title: string;
@PositiveNumber
price: number;
constructor(t: string, p: number) {
this.title = t;
this.price = p;
}
}
const courseForm = document.querySelector("form")!;
courseForm.addEventListener("submit", (event) => {
event.preventDefault();
const titleEl = document.getElementById("title") as HTMLInputElement;
const priceEl = document.getElementById("price") as HTMLInputElement;
const title = titleEl.value;
const price = +priceEl.value;
const createdCourse = new Course(title, price);
if (!validate(createdCourse)) {
throw new Error("Invalid Input, plz try again");
return;
}
console.log(createdCourse);
});
interface ValidatorConfig {
[property: string]: {
[validatableProp: string]: string[]; // ['required', 'positive']
}; // property(검사할 프로퍼티가 있는 클래스 이름)
}
const registeredValidators: ValidatorConfig = {}; // 빈 객체로 초기화
function Required(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: ["required"],
}; // Course
}
function PositiveNumber(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: ["positive"],
};
}
function validate(obj: any) {
console.log(obj);
// 등록된 검사기를 모두 살펴보고 자신에게 필요한 검사 항목을 찾아 해당 로직을 수행
const objValidatorConfig = registeredValidators[obj.constructor.name];
if (!objValidatorConfig) {
return true; // 객체가 유효하다.
}
let isValid = true;
for (const prop in objValidatorConfig) {
console.log(prop);
for (const validator of objValidatorConfig[prop]) {
switch (validator) {
case "required":
isValid = isValid && !!obj[prop];
break;
case "positive":
isValid = isValid && obj[prop] > 0;
break;
}
}
}
return isValid;
}
class Course {
@Required
title: string;
@PositiveNumber
price: number;
constructor(t: string, p: number) {
this.title = t;
this.price = p;
}
}
const courseForm = document.querySelector("form")!;
courseForm.addEventListener("submit", (event) => {
event.preventDefault();
const titleEl = document.getElementById("title") as HTMLInputElement;
const priceEl = document.getElementById("price") as HTMLInputElement;
const title = titleEl.value;
const price = +priceEl.value;
const createdCourse = new Course(title, price);
if (!validate(createdCourse)) {
throw new Error("Invalid Input, plz try again");
return;
}
console.log(createdCourse);
});