TypeScript 를 활용한

안전한 코드 만들기

https://slides.com/woongjae/lgcns-ts-3

Lead Software Engineer @ProtoPie

Microsoft MVP

TypeScript Korea User Group Organizer

Marktube (Youtube)

Mark Lee

이 웅재

1. Enum Types 와 never 활용

2. Class Types

3. Generic

4. Method Overloads
5. Decorators

6. Conditional Types

1. Enum Types 와 never 활용

Toast 는 모두 type 을 가지고 type 은 enum 중 하나를 가진다.

enum ToastType {
    AFTER_SAVED,
    AFTER_PUBLISHED,
    AFTER_RESTORE,
}

interface Toast {
    type: ToastType,
    createdAt: string,
}

const toasts: Toast[] = [...];

if 와 else if 로 이루어진 잘못된 추론

// toastNodes1 -> (JSX.Element | undefined)[]
const toastNodes1 = toasts.map((toast) => {
  if (toast.type === ToastType.AFTER_SAVED)
    return (
      <div key={toast.createdAt}>
        <AfterSavedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_PUBLISHED)
    return (
      <div key={toast.createdAt}>
        <AfterPublishedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_RESTORE)
    return (
      <div key={toast.createdAt}>
        <AfterRestoredToast />
      </div>
    );
});

if, else if, else 로 작동하는 추론

// toastNodes2 -> JSX.Element[]
const toastNodes2 = toasts.map((toast) => {
  if (toast.type === ToastType.AFTER_SAVED)
    return (
      <div key={toast.createdAt}>
        <AfterSavedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_PUBLISHED)
    return (
      <div key={toast.createdAt}>
        <AfterPublishedToast />
      </div>
    );
  else
    return (
      <div key={toast.createdAt}>
        <AfterRestoredToast />
      </div>
    );
});

마지막 else 에 never 를 검사함

// toastNodes3 -> JSX.Element[]
const toastNodes3 = toasts.map((toast) => {
  if (toast.type === ToastType.AFTER_SAVED)
    return (
      <div key={toast.createdAt}>
        <AfterSavedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_PUBLISHED)
    return (
      <div key={toast.createdAt}>
        <AfterPublishedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_RESTORE)
    return (
      <div key={toast.createdAt}>
        <AfterRestoredToast />
      </div>
    );
  else return neverExpected(toast.typs);
});

function neverExpected(value: never): never {
  throw new Error(`Unexpected value : ${value}`);
}

if + return 과 마지막에 never 를 검사함

// toastNodes4 -> JSX.Element[]
const toastNodes4 = toasts.map((toast) => {
  if (toast.type === ToastType.AFTER_SAVED)
    return (
      <div key={toast.createdAt}>
        <AfterSavedToast />
      </div>
    );
  if (toast.type === ToastType.AFTER_PUBLISHED)
    return (
      <div key={toast.createdAt}>
        <AfterPublishedToast />
      </div>
    );
  if (toast.type === ToastType.AFTER_RESTORE)
    return (
      <div key={toast.createdAt}>
        <AfterRestoredToast />
      </div>
    );

  return neverExpected(toast.typs);
});

switch 와 default never 를 통한 처리

const toastNodes5 = toasts.map((toast) => {
  switch (toast.type) {
    case ToastType.AFTER_SAVED:
      return (
        <div key={toast.createdAt}>
          <AfterSavedToast />
        </div>
      );
    case ToastType.AFTER_PUBLISHED:
      return (
        <div key={toast.createdAt}>
          <AfterPublishedToast />
        </div>
      );
    case ToastType.AFTER_RESTORE:
      return (
        <div key={toast.createdAt}>
          <AfterRestoredToast />
        </div>
      );
    default:
      return neverExpected(toast.type);
  }
});

2. Class Types

Class Property 의 타입을 명시적으로 지정해야 한다.

// v3.9.7

class Square1 {
  area; // error! implicit any
  sideLength; // error! implicit any
}

클래스 만들기

  • 생성자 함수가 없으면, 디폴트 생성자가 불린다.

  • 클래스의 프로퍼티 혹은 멤버 변수가 정의되어 있지만, 값을 대입하지 않으면 undefined 이다.

  • 접근제어자 (Access Modifier) 는 public 이 디폴트 이다.

class Person1 {
  name!: string;
  age!: number;
}

const person: Person1 = new Person1();
console.log(person); // Person1 {}
person.age = 38;
console.log(person.name); // undefined

클래스와 프로퍼티 (1)

class Person2 {
  name!: string;
  age!: number;

  constructor() {
    console.log(this.name === null); // false
    console.log(this.name === undefined); // true
  }
}

const person2: Person2 = new Person2();
person2.name = 'Mark';
person2.age = 38;
console.log(person2); // Person2 {name: 'Mark', age: 38}

클래스와 프로퍼티 (2)

  • 클래스의 프로퍼티를 선언과 동시에 값을 할당하는 방법도 있다.

  • 생성자가 불리기 전에 이미 프로퍼티의 값이 저장되어 있음을 알 수 있다.

class Person3 {
  name: string = 'Mark';
  age: number = 38;

  constructor() {
    console.log(this.name); // 'mark'
  }
}

const person3: Person3 = new Person3();
console.log(person3); // Person3 {name: 'Mark', age: 38}

클래스와 프로퍼티의 접근 제어자

  • private 으로 설정된 프로퍼티는 dot 으로 접근할 수 없다.

  • 클래스 내부에서는 private 프로퍼티를 사용할 수 있다.

  • private 이 붙은 변수나 함수는 _ 를 이름앞에 붙이는데, 이는 문법이 아니라 널리 쓰이는 코딩 컨벤션이다.

class Person4 {
  public name!: string;
  private _age: number;

  constructor(age: number) {
    this._age = age;
  }
}

const person4: Person4 = new Person4(38);
person4.name = 'Mark';
// person._age (X)
console.log(person4); // Person {name: 'Mark', _age: 38}

클래스와 상속 그리고 프로퍼티의 접근 제어자

  • 부모에서 private 으로 설정된 프로퍼티는 상속을 받은 자식에서도 접근할 수 없다.

  • 부모에서 protected 로 설정된 프로퍼티는 상속을 받은 자식에서 접근이 가능하다.

  • 상속을 받은 자식 클래스에서 부모 클래스에 this 를 통해 접근하려면, 생성자에서 super(); 를 통해 초기화 해야한다.

class Parent5 {
  private privateProp!: string;
  protected protectedProp!: string;

  constructor() {}
}

class Child5 extends Parent5 {
  constructor() {
    super();

    this.protectedProp = 'protected';
    // this.privateProp = 'private'; // (X)
  }
}

const child5 = new Child5();
console.log(child5);

클래스와 디폴트 생성자

  • 디폴트 생성자는 프로그래머가 만든 생성자가 없을 때 사용할 수 있다.

  • 사용자가 만든 생성자가 하나라도 있으면, 디폴트 생성자는 사라진다.

  • 생성자에서 바로 멤버로 할당할 수 있는 방법이 있다.

class Person6 {
  constructor(public name: string, private _age: number) {}
}

const person6: Person6 = new Person6('Mark', 38);
// const person6: Person6 = new Person6();

클래스와 메서드

  • 클래스 내부에 작성된 메서드는 public 이 디폴트

  • arrow function 으로 작성 가능

  • private 을 이용하면 클래스 외부애서 접근 불가

class Person7 {
  constructor(private _name: string, private _age: number) {}

  print(): void {
    console.log(`이름은 ${this._name} 이고, 나이는 ${this._age} 살 입니다.`);
  }

  printName = (): void => {
    console.log(`이름은 ${this._name} 입니다.`);
  };

  private printAge(): void {
    console.log(`나이는 ${this._age} 살 입니다.`);
  }
}

const person: Person7 = new Person7('Mark', 36);
person.print(); // 이름은 Mark 이고, 나이는 36 살 입니다.
person.printName(); // 이름은 Mark 입니다.
// person.printAge(); // (X)

클래스와 상속 (1)

  • 상속은 extends 키워드를 이용한다.

  • 자식 클래스에서 디폴트 생성자는 부모의 생성자와 입력 형태가 같다.

  • 접근제어자의 오버라이드

class Parent8 {
  constructor(protected _name: string, protected _age: number) {}

  print(): void {
    console.log(`이름은 ${this._name} 이고, 나이는 ${this._age} 살 입니다.`);
  }

  printName = (): void => {
    console.log(`이름은 ${this._name} 입니다.`);
  };

  private printAge(): void {
    console.log(`나이는 ${this._age} 살 입니다.`);
  }
}

class Child8 extends Parent8 {
  _name = 'Mark Jr.';
}

// const p: Child8 = new Child8(); // (X)
const child8: Child8 = new Child8('', 5);
child8.print(); // 이름은 Mark Jr. 이고, 나이는 5 살 입니다.
console.log(child8._name);

클래스와 상속 (2)

  • 생성자를 정의하고, this 를 사용하려면, super 를 통해 부모의 생성자를 호출해줘야 한다.

  • super 를 호출할때는 부모 생성자의 입력 타입이 같아야 한다.

  • super 를 호출하는 것은 클래스 외부에서 호출하는 것과 같다.

  • protected 함수를 호출해서 그 안의 private 을 출력하는 것에 주의한다.

class Parent9 {
  constructor(protected _name: string, private _age: number) {}

  print(): void {
    console.log(`이름은 ${this._name} 이고, 나이는 ${this._age} 살 입니다.`);
  }

  protected printName = (): void => {
    console.log(`이름은 ${this._name} 입니다.`);
  };

  protected printAge(): void {
    console.log(`나이는 ${this._age} 살 입니다.`);
  }
}

class Child9 extends Parent9 {
  constructor(age: number) {
    super('Mark Jr.', age);

    this.printName();
    this.printAge();
  }
}

const child9: Child9 = new Child9(1); // 이름은 Mark Jr. 입니다. 나이는 1 살 입니다.

클래스와 getter, setter

  • _ 를 변수명 앞에 붙이고, 내부에서만 사용한다.

  • getter 를 함수처럼 설정하면, 프로퍼티처럼 꺼내쓸수있다.

  • 마찬가지로 setter 를 함수처럼 설정하면, 추가 작업을 하고 셋팅할 수 있다.

class Person10 {
  private _name: string;
  private _age: number;

  constructor(name: string, age: number) {
    this._name = name;
    this._age = age;
  }

  get name() {
    return this._name;
  }

  set name(name: string) {
    // 작업
    this._name = `${name} Lee`;
  }
}

const person10: Person10 = new Person10('Mark', 36);

console.log(person10.name);
person10.name = 'Woongjae';
console.log(person10.name);

클래스와 static 프로퍼티 => 클래스 변수

  • static 키워드를 붙힌 프로퍼티는 클래스.프로퍼티로 사용한다.
  • static 프로퍼티에 private, protected 를 붙히면 똑같이 동작한다.
class Person11 {
  public static CITY = '';
  private static lastName: string = 'Lee';
  private _name: string;
  private _age: number;

  constructor(name: string, age: number) {
    this._name = name;
    this._age = age;
  }

  public print() {
    console.log(`${this._name} ${Person11.lastName} in ${Person11.CITY}.`);
  }
}

const person11: Person11 = new Person11('Mark', 36);
Person11.CITY = 'Seoul';
person11.print(); // Mark Lee in Seoul.

클래스와 static 메서드 => 클래스 멤버 함수

class Person12 {
  public static Talk(): void {
    console.log('안녕하세요.');
  }
}

Person12.Talk(); // 안녕하세요.

모듈에서 private static 프로퍼티 혹은 메서드

/*
class Person13 {
  private static PROPERTY = '프라이빗 프로퍼티';
  private static METHOD() {
    console.log('프라이빗 메서드');
  }

  constructor() {
    console.log(Person13.PROPERTY);
    Person13.METHOD();
  }
}
*/

const PROPERTY = '모듈 내 변수';
function METHOD() {
  console.log('모듈 내 함수');
}

export class Person13 {
  constructor() {
    console.log(PROPERTY);
    METHOD();
  }

Abstract Class

  • abstract 키워드가 사용된 클래스는 new 로 생성할 수 없다.

  • abstract 키워드가 사용된 클래스를 상속하면 abstract 키워드가 붙은 함수를 구현해야 한다.

abstract class AbstractPerson14 {
  protected _name: string = 'Mark';
  abstract setName(name: string): void;
}

class Person14 extends AbstractPerson14 {
  setName(name: string): void {
    this._name = name;
  }
}

// const person14 = new AbstractPerson14(); // (X)
const person14 = new Person14();

Class 와 private constructor

  • 생성자 함수 앞에 접근제어자인 private 을 붙일 수 있다.

  • 하지만 외부에서 생성이 불가능하다.

class PrivateClass {
  private constructor() {}
}

// const p: PrivateClass = new PrivateClass(); (X)

Class 와 싱글톤 패턴

  • private 생성자를 이용해서 내부에서만 인스턴스 생성이 가능하도록 함.

  • pubilc static 메서드를 통해 private static 인스턴스 레퍼런스를 획득한다.

  • Lazy Loading (Initialization) : 최초 실행시가 아니라, 사용시에 할당을 함

/*
class Preference {
  public static getInstance(): Preference {
    if (Preference.instance === null) {
      Preference.instance = new Preference();
    }

    return Preference.instance;
  }
  private static instance: Preference | null = null;
  private constructor() {}
}

const p: Preference = Preference.getInstance();
*/

class PreferenceClass {
  constructor() {}
}

export const Preference = new PreferenceClass();
export type PreferenceType = typeof Preference;

Class 와 readonly

  • private readonly 로 선언된 경우, 생성자에서는 할당이 가능하다.

  • private readonly 로 선언된 경우, 생성자 이외에서는 할당이 불가능하다.

  • public readonly 로 선언된 경우, 클래스 외부에서는 다른값을 할당할 수 없다.

  • 마치 getter 만 있는 경우와 같다.

class Person17 {
  private readonly _name: string;
  public readonly age: number = 36;

  constructor(name: string) {
    this._name = name;
  }

  public setName(name: string) {
    // this._name = name; (X)
  }
}

const person17: Person17 = new Person17('Mark');
console.log(person17.age);
// person17.age = 37; // (X)

Class Property 의 타입을 명시적으로 지정했지만

// v3.9.7

class Square2 {
  area: number;
  sideLength: number;
}

const square2 = new Square2();
console.log(square2.area); // compile time - number, runtime - undefined
console.log(square2.sideLength); // compile time - number, runtime - undefined

strictPropertyInitialization 옵션을 켜면


Class 의 Property 가 생성자 혹은 선언에서 값이 지정되지 않으면,

컴파일 에러를 발생시켜 주의를 준다.

Class Property 가 초기화되지 않았다.

// v3.9.7

class Square2 {
  area: number; // error TS2564: Property 'area' has no initializer and is not definitely assigned in the constructor.
  sideLength: number; // error TS2564: Property 'sideLength' has no initializer and is not definitely assigned in the constructor.
}

// 사용자는 시도조차 할 수 없도록 만듭니다.

const square2 = new Square2();
console.log(square2.area);
console.log(square2.sideLength);

Class Property 가 선언에서 초기화

// v3.9.7

class Square3 {
  area: number = 0;
  sideLength: number = 0;
}

Class Property 가 생성자에서 초기화

// v3.9.7

class Square4 {
  area: number;
  sideLength: number;

  constructor(sideLength: number) {
    this.sideLength = sideLength;
    this.area = sideLength ** 2;
  }
}

Class Property 의 타입 추론

// v4.0.2

class Square5 {
  area; // 4 부터는 any 가 아니라, 생성자에 의해 추론된다.
  sideLength; // 4 부터는 any 가 아니라, 생성자에 의해 추론된다.

  constructor(sideLength: number) {
    this.sideLength = sideLength;
    this.area = sideLength ** 2;
  }
}

Class Property 의 타입 추론

// v4.0.2

class Square6 {
  sideLength;

  constructor(sideLength: number) {
    if (Math.random()) {
      this.sideLength = sideLength;
    }
  }

  get area() {
    return this.sideLength ** 2; // error! Object is possibly 'undefined'.
  }
}

여전히 생성자를 벗어나면 추론되지 않는다.

// v4.0.2

class Square7 {
  sideLength!: number; // ! 로 의도를 표현해야 한다.

  constructor(sideLength: number) {
    this.initialize(sideLength);
  }

  initialize(sideLength: number) {
    this.sideLength = sideLength;
  }

  get area() {
    return this.sideLength ** 2;
  }
}

3. Generic

Generic - Any 와 다른 점

  • hello 의 리턴이 any 이기 때문에 타입 헬퍼가 제대로 되지 않음

  • helloGeneric 을 사용하면 정상적으로 사용가능

function helloString(message: string): string {
  return message;
}

function helloNumber(message: number): number {
  return message;
}

// 더 많은 반복된 함수들 ...

function hello(message: any): any {
  return message;
}

function helloGeneric<T>(message: T): T {
  return message;
}

console.log(hello('Mark').length);
console.log(hello(38).length); // undefined

console.log(helloGeneric('Mark').length);
// console.log(helloGeneric<number>('Mark').length); (X)
console.log(helloGeneric(38).toString());
// console.log(helloGeneric(36).length); (X)

Generic Basic

  • Generic 타입을 쓰지 않으면, T 를 추론

  • Generic 타입을 쓰면, T 를 검증

function helloBasic<T>(message: T): T {
  return message;
}

console.log(helloBasic<string>('Mark'));
const age = helloBasic(38);
// helloBasic<number>('38'); (X)

Generic Array & Tuple

function helloArray<T>(messages: T[]): T {
  return messages[0];
}

function helloTuple<T, K>(messages: [T, K]): T {
  return messages[0];
}

console.log(helloArray(['Hello', 'World'])); // string[]
console.log(helloArray(['Hello', 1])); // Array<string | number>
console.log(helloTuple(['Hello', 'World'])); // [string, string]
console.log(helloTuple(['Hello', 1])); // [string, number]
// console.log(helloTuple(['Hello', 'world', 1])); // Error

Generic Function

type HelloFunctionGeneric = <T>(message: T) => T;

const helloFunction: HelloFunctionGeneric = <T>(message: T): T => {
  return message;
};

console.log(helloFunction<string>('Hello').length);

Generic Class

class Person<T> {
  private _name: T;

  constructor(name: T) {
    this._name = name;
  }
}

new Person('Mark');
// new Person<string>(38); (X)

Generic with extends

class Person6<T extends string | number> {
  private _name: T;

  constructor(name: T) {
    this._name = name;
  }
}

new Person6('Mark');
new Person6(38);
// new Person6(true); // T 가 string 또는 number 를 상속받기 때문에 boolean 은 X

Generic with multiple types

class Person7<T, K> {
  private _name: T;
  private _age: K;

  constructor(name: T, age: K) {
    this._name = name;
    this._age = age;
  }
}

new Person7('Mark', 38);

keyof & type lookup system

interface Person8 {
  name: string;
  age: number;
}

const person8: Person8 = {
  name: 'Mark',
  age: 36
};

function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
  obj[key] = value;
}

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

setProperty(person8, 'name', 'Anna');
// setProperty(person8, 'name', 27);
console.log(getProperty(person8, 'name'));
// console.log(getProperty(person8, 'fullname'));

4. Method Overloads

오버로딩이 불가능한 자바스크립트에 타입을 붙이는 경우

function shuffle(value: string | any[]): string | any[] {
  if (typeof value === 'string')
    return value
      .split('')
      .sort(() => Math.random() - 0.5)
      .join('');
  return value.sort(() => Math.random() - 0.5);
}

console.log(shuffle('Hello, Mark!')); // string | any[]
console.log(shuffle(['Hello', 'Mark', 'long', 'time', 'no', 'see'])); // string | any[]
console.log(shuffle([1, 2, 3, 4, 5])); // string | any[]

제네릭을 떠올리자! => conditional type 활용

function shuffle2<T extends string | any[]>(
  value: T,
): T extends string ? string : T;
function shuffle2(value: any) {
  if (typeof value === 'string')
    return value
      .split('')
      .sort(() => Math.random() - 0.5)
      .join('');
  return value.sort(() => Math.random() - 0.5);
}

// function shuffle2<"Hello, Mark!">(value: "Hello, Mark!"): string
shuffle2('Hello, Mark!');

// function shuffle2<string[]>(value: string[]): string[]
shuffle2(['Hello', 'Mark', 'long', 'time', 'no', 'see']);

// function shuffle2<number[]>(value: number[]): number[]
shuffle2([1, 2, 3, 4, 5]);

// error! Argument of type 'number' is not assignable to parameter of type 'string | any[]'.
shuffle2(1);

오버로딩을 활용하면?

function shuffle3(value: string): string;
function shuffle3<T>(value: T[]): T[];
function shuffle3(value: string | any[]): string | any[] {
  if (typeof value === 'string')
    return value
      .split('')
      .sort(() => Math.random() - 0.5)
      .join('');
  return value.sort(() => Math.random() - 0.5);
}

shuffle3('Hello, Mark!');
shuffle3(['Hello', 'Mark', 'long', 'time', 'no', 'see']);
shuffle3([1, 2, 3, 4, 5]);

클래스의 메서드 오버로딩 : 작성자

class ExportLibraryModal {
  public openComponentsToLibrary(
    libraryId: string,
    componentIds: string[],
  ): void;
  public openComponentsToLibrary(componentIds: string[]): void;
  public openComponentsToLibrary(
    libraryIdOrComponentIds: string | string[],
    componentIds?: string[],
  ): void {
    if (typeof libraryIdOrComponentIds === 'string') {
      if (componentIds !== undefined) { // 이건 좀 별루지만,
        // 첫번째 시그니처
        libraryIdOrComponentIds;
        componentIds;
      }
    }

    if (componentIds === undefined) { // 이건 좀 별루지만,
      // 두번째 시그니처
      libraryIdOrComponentIds;
    }
  }
}

클래스의 메서드 오버로딩 : 사용자

const modal = new ExportLibraryModal();

modal.openComponentsToLibrary(
  'library-id',
  ['component-id-1', 'component-id-1'],
);

modal.openComponentsToLibrary(['component-id-1', 'component-id-1']);

5. Decorators

Decorator 란 ?

  • 함수다.

  • 컴파일 타임에는 그 함수의 타입만 체크한다.

  • 런타임에 사용 및 처리가 된다.

  • 클래스, 메서드, 프로퍼티, 메서드의 파라미터에 사용할 수 있다.

  • 클래스가 인스턴스로 만들어질 때가 아니라, 최초 클래스가 참조될 때, 한번만 적용된다.

  • experimentalDecorators 옵션

Class Decorator - 기본 구조

// Basic
function classDecorator<T extends { new (...args: any[]): {} }>(constructorFn: T) {...}

function classDecoratorFactory(...) {
	return function<T extends { new (...args: any[]): {} }>(constructorFn: T) {...}
}

@classDecorator
class Test {
}

@classDecorator()
class Test {
}
  
/*
# constructorFn

- constructorFn
- return class
*/

Class Decorator Example1

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

    public print() {
      console.log('this is print');
    }
  };
}

@classDecorator
class Test1 {}

(new Test1() as any).print();

Class Decorator Example2

function classDecoratorFactory(arg: string) {
  return function<T extends { new (...args: any[]): {} }>(constructorFn: T) {
    constructorFn.prototype.print2 = function() {
      console.log('this is print2', arg);
    };
    constructorFn.prototype.gender = 'male';
    return class extends constructorFn {
      public name = 'mark';
      private _age = 36;

      constructor(...args: any[]) {
        super(args);
      }

      public print() {
        console.log('this is print', arg);
      }
    };
  };
}

@classDecoratorFactory('what')
class Test2 {}

const test2 = new Test2();
(test2 as any).print(); // this is print what
(test2 as any).print2(); // this is print2 what
console.log(Test2.prototype); // class_1 { constructor: [Function: class_1], print: [Function] }
console.log(test2); // class_1 { name: 'mark', _age: 36 }
console.log(Object.keys(test2)); // [ 'name', '_age' ]
console.log((test2 as any).gender); // male

Method Decorator - 기본 구조

function methodDecorator(
	target: any,
	propertyKey: string,
	descriptor: PropertyDescriptor
) {...}

function methodDecoratorFactory(...) {
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {...};
}

target, propertyKey, descriptor

function methodDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log('target', target);
  console.log('propertyKey', propertyKey);
  console.log('descriptor', descriptor);
}

class Test3 {
  @methodDecorator
  public print() {}
}

// target Test3 { print: [Function] }
// propertyKey print
// descriptor { value: [Function], writable: true, enumerable: true, configurable: true }

Method Decorator Example 1

function methodDecoratorFactory(canBeEdit: boolean = false) {
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.writable = canBeEdit;
  };
}

class Test4 {
  @methodDecoratorFactory()
  first() {
    console.log('first original');
  }

  @methodDecoratorFactory(true)
  second() {
    console.log('second original');
  }

  @methodDecoratorFactory(false)
  third() {
    console.log('third original');
  }
}

const test4 = new Test4();
test4.first = function() {
  console.log('first new');
}; // runtime error
test4.second = function() {
  console.log('second new');
};// runtime error
test4.third = function() {
  console.log('third new');
};

PropertyDescriptor 조작

configurable

  • 이 속성기술자는 해당 객체로부터 그 속성을 제거할 수 있는지를 기술한다.

  • true 라면 삭제할 수 있다. 기본값은 false.

enumerable

  • 해당 객체의 키가 열거 가능한지를 기술한다.

  • true 라면 열거가능하다. 기본값은 false.

value

  • 속성에 해당되는 값으로 오직 적합한 자바스크립트 값 (number, object, function, etc) 만 올 수 있다. 기본값은 undefined.

writable

  • writable이 true로 설정되면 할당연산자 assignment operator 를 통해 값을 바꿀 수 있다. 기본값은 false.

Method Decorator Example 2

function log(show: boolean = true) {
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    descriptor.value = function(...args: any[]) {
      show && console.log('start');
      original(...args);
      show && console.log('end');
    };
  };
}

class Test5 {
  @log()
  print1(first: string, second: number) {
    console.log('print1', first, second);
  }

  @log(false)
  print2(first: string, second: number) {
    console.log('print2', first, second);
  }
}

const test5 = new Test5();
test5.print1('mark', 36);
test5.print2('mark', 36);

Property Decorator - 기본 구조

function propertyDecorator(target: any, propName: string) {...}

function propertyDecoratorFactory(...) {
	return function(target: any, propName: string) {...}
}

target, propName

  • target

  • propName

Property Decorator Example1

function propertyDecorator(target: any, propName: string): any {
  console.log(target);
  console.log(propName);
  return {
    writable: false
  };
}

class Test6 {
  @propertyDecorator
  name: string = 'Mark';
}

const test6 = new Test6();
test6.name = 'Anna'; // runtime error

Parameter Decorator - 기본 구조

function parameterDecorator(
	target: any,
	methodName: string,
	paramIndex: number
) {...}

function parameterDecoratorFactory(...) {
	return function(
		target: any,
		methodName: string,
		paramIndex: number
	) {...}
}

target, methodName, paramIndex

  • target

  • methodName

  • paramIndex

Parameter Decorator Example1

function parameterDecorator(
	target: any,
	methodName: string,
	paramIndex: number
) {
	console.log('parameterDecorator start');
  console.log(target);
  console.log(methodName);
  console.log(paramIndex);
  console.log('parameterDecorator end');
}

class Test7 {
	private _name: string;
	private _age: number;

	constructor(name: string, @parameterDecorator age: number) {
		this._name = name;
		this._age = age;
	}

	print(@parameterDecorator message: string) {
		console.log(message);
	}
}

Parameter Decorator Example1

// parameterDecorator start
// Test7 { print: [Function] }
// print
// 0
// parameterDecorator end
// parameterDecorator start
// [Function: Test7]
// undefined
// 1
// parameterDecorator end

6. Conditional Types

Item<T> - T 에 따라 달라지는 container

interface StringContainer {
  value: string;
  format(): string;
  split(): string[];
}

interface NumberContainer {
  value: number;
  nearestPrime: number;
  round(): number;
}

type Item1<T> = {
  id: T,
  container: any;
};

const item1: Item1<string> = {
  id: "aaaaaa",
  container: null
};

Item<T>

T 가 string 이면 StringContainer, 아니면 NumberContainer

type Item2<T> = {
  id: T;
  container: T extends string ? StringContainer : NumberContainer;
};

const item2: Item2<string> = {
  id: 'aaaaaa',
  container: null, // Type 'null' is not assignable to type 'StringContainer'.
};

Item<T>

T 가 string 이면 StringContainer

T 가 number 면 NumberContainer

아니면 사용 불가

type Item3<T> = {
  id: T extends string | number ? T : never;
  container: T extends string
    ? StringContainer
    : T extends number
    ? NumberContainer
    : never;
};

const item3: Item3<boolean> = {
  id: true, // Type 'boolean' is not assignable to type 'never'.
  container: null, // Type 'null' is not assignable to type 'never'.
};

ArrayFilter<T>

type ArrayFilter<T> = T extends any[] ? T : never;

type StringsOrNumbers = ArrayFilter<string | number | string[] | number[]>;

// 1. string | number | string[] | number[]
// 2. never | never | string[] | number[]
// 3. string[] | number[]

Table or Dino

interface Table {
  id: string;
  chairs: string[];
}

interface Dino {
  id: number;
  legs: number;
}

interface World {
  getItem<T extends string | number>(id: T): T extends string ? Table : Dino;
}

let world: World = null as any;

const dino = world.getItem(10);
const what = world.getItem(true); // Error! Argument of type 'boolean' is not assignable to parameter of type 'string | number'.ts(2345)

Flatten<T>

type Flatten<T> = T extends any[]
  ? T[number]
  : T extends object
  ? T[keyof T]
  : T;


const numbers = [1, 2, 3];
type NumbersArrayFlattened = Flatten<typeof numbers>;
// 1. number[]
// 2. number

const person = {
  name: 'Mark',
  age: 38
};
                             
type SomeObjectFlattened = Flatten<typeof person>;
// 1. keyof T --> "id" | "name"
// 2. T["id" | "name"] --> T["id"] | T["name"] --> number | string

const isMale = true;
type SomeBooleanFlattened = Flatten<typeof isMale>;
// true

infer

type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;
const promises = [Promise.resolve('Mark'), Promise.resolve(38)];

type Expected = UnpackPromise<typeof promises>; // string | number

함수의 리턴 타입 알아내기 - MyReturnType

function plus1(seed: number): number {
  return seed + 1;
}

type MyReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

type Id = MyReturnType<typeof plus1>;

lookupEntity(plus1(10));

function lookupEntity(id: Id) {
  // query DB for entity by ID
}

내장 conditional types (1)

// type Exclude<T, U> = T extends U ? never : T;
type Excluded = Exclude<string | number, string>; // number - diff

// type Extract<T, U> = T extends U ? T : never;
type Extracted = Extract<string | number, string>; // string - filter

// Pick<T, Exclude<keyof T, K>>; (Mapped Type)
type Picked = Pick<{name: string, age: number}, 'name'>;

// type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Omited = Omit<{name: string, age: number}, 'name'>;

// type NonNullable<T> = T extends null | undefined ? never : T;
type NonNullabled = NonNullable<string | number | null | undefined>;

내장 conditional types (2)

/*
type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;
*/

/*
type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;
*/
type MyParameters = Parameters<(name: string, age: number) => void>; // [name: string, age: number]

내장 conditional types (3)

interface Constructor {
  new (name: string, age: number): string;
}

/*
type ConstructorParameters<
  T extends new (...args: any) => any
> = T extends new (...args: infer P) => any ? P : never;
*/

type MyConstructorParameters = ConstructorParameters<Constructor>; // [name: string, age: number]

/*
type InstanceType<T extends new (...args: any) => any> = T extends new (
  ...args: any
) => infer R
  ? R
  : any;
*/

type MyInstanceType = InstanceType<Constructor>; // string

Function 인 프로퍼티 찾기

type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

interface Person {
  id: number;
  name: string;
  hello(message: string): void;
}

type T1 = FunctionPropertyNames<Person>;
type T2 = NonFunctionPropertyNames<Person>;
type T3 = FunctionProperties<Person>;
type T4 = NonFunctionProperties<Person>;

TypeScript 를 활용한 안전한 코드 만들기

By Woongjae Lee

TypeScript 를 활용한 안전한 코드 만들기

LG CNS 타입스크립트 특강 3주차

  • 1,124