Typescript 기초 팁

이영민·2023년 5월 23일
0

자바스크립트 기본

블록 수준 스코프

let: 재할당이 가능한 블록 레벨 변수.

  • var와 달리 변수의 정의문이 평가되기 전 해당 변수 참조 시 ReferenceError 발생(Temporal Dead Zone). (호이스팅 자체는 이루어진다고 함).

const: 재할당이 불가능한 블록 레벨 변수.

  • 재할당이 불가능할 뿐, 불변값이 아니므로 객체 내부는 조작 가능.

Best Practice
1. const를 기본적으로 사용.
2. 재할당이 반드시 필요할 경우에만 let으로 선언.

for-of, for-in

for-in은 객체의 property를 순회함.

  • array 또한 내부적으로는 각 index를 property로 가짐. 즉, array를 for-in으로 순회하면 0, 1, 2, ... 를 반환.
    • property attribute의 Enumerable 값이 false이거나, key가 symbol type인 property들은 반환되지 않음.
      • Enumerable 값이 false인 경우는 Object.getOwnPropertyNames, key가 symbol type인 경우 Object.getOwnPropertySymbols로 취득 가능.

for-of는 이터러블(프로토콜을 구현한) 객체를 순회함.

  • array를 순회할 경우 일반적으로 기대한 바와 같이 0번 index부터 차례로 value를 반환.
  • 이터러블 프로토콜: Symbol.iterator를 key값으로 갖는 메서드를 가지며, 이 메서드를 실행할 시 이터레이터(프로토콜을 구현한) 객체가 반환됨.
    • 이터레이터 프로토콜: 특정 조건을 만족하는 next() 메서드를 가질 시.

화살표 함수

타 언어에서의 람다 함수.
function으로 선언되는 일반적인 함수와의 차이: this, super, arguments, prototype을 가지지 않음.

  • this가 없으므로 상위 환경에서 this를 검색 -> 화살표 함수가 선언되었을 때 this가 static하게 정해지는 꼴.
const obj = {
  a: 1,
  normalFunc: function() { console.log(this); },
  arrowFunc: () => { console.log(this); },
};
const { normalFunc, arrowFunc } = obj;
obj.normalFunc(); // { 
                  //   a: 1,
                  //   normalFunc: [Function: normalFunc],
                  //   arrowFunc: [Function: arrowFunc] 
                  // }
normalFunc(); // undefined
obj.arrowFunc(); // (global object)
arrowFunc(); // (global object)
  • prototype을 가지지 않으므로 constructor로 사용할 수 없음.

비동기 처리

기존 비동기 처리 방식: 비동기 서브루틴에 콜백 함수를 넘김.

  • 단점: 콜백 지옥.

프로미스: 생성자의 인자로 resolve, reject 두 핸들러를 인자로 받는 함수를 받음. 함수 본문에서 특정 로직 실행 후, resolve, reject 수행하게 하면 됨.

  • then: resolve가 호출된 경우, 즉 해당 비동기 작업이 완료된 경우의 핸들러.
  • catch: reject가 호출된 경우, 즉 해당 비동기 작업이 거부된 경우의 핸들러.

then과 catch는 프로미스를 반환하므로 프로미스 체인을 작성할 수 있음. 프로미스 체인의 다음 프로미스는 이전 프로미스가 반환한 값으로 resolve를 호출.

  • 반환한 값이 프로미스일 경우 해당 프로미스가 resolved될 때 다음 then 함수가 호출되며, resolve에 전달된 값이 다음 then 함수에 전달됨.
function errorHandler(err) {
  if (err) {
    console.log(err);
  }
}
fetchDocument(url)
.then(document => fetchAuthor(document), errorHandler)
.then(author => fetchPostsFromAuthor(author), errorHandler)
.then(posts => /* do something with posts */, errorHandler);
  • 에러가 던져질 경우 가장 가까운 catch가 전달 받음.

제네레이터: 출구가 여럿인 함수. 함수 내부에서 yield를 마주할 때마다 해당 값을 밖으로 반환하고 수행을 멈춤. 제네레이터 객체의 next를 호출하면 다음 코드 수행.

function* idMaker() {
    let index = 0
    while (index < 3) yield index++
}

let gen = idMaker()

console.log(gen.next()) // { value: 0, done: false }
console.log(gen.next()) // { value: 1, done: false }
console.log(gen.next()) // { value: 2, done: false }
console.log(gen.next()) // { done: true }

async, await: 프로미스, 제네레이터를 활용하여 비동기 프로그래밍을 마치 동기 프로그래밍처럼 수행하게 해줌.

async function foo() {
    try {
        var val = await getMeAPromise()
        console.log(val)
    } catch (err) {
        console.log('Error: ', err.message)
    }
}
  • async: 비동기 함수 정의. 해당 함수가 반환하는 값은 암시적으로 프로미스로 감싸짐.
  • await: 해당 키워드가 붙은 표현식 expr은 다음과 같은 값을 가짐.
    • 뒤따르는 값이 프로미스가 아닐 시, expr은 그 값 그대로 가짐.
    • 뒤따르는 값이 프로미스일 시, 해당 프로미스가 처리될 때까지 실행을 중지하고, 프로미스가 완료될 경우 expr은 resolve의 인자로 사용된 값을 가짐. 프로미스가 거부될 시 오류를 그대로 위로 던짐.

JS에서의 Prototype을 이용한 클래스 상속 구현

var __extends =
    this.__extends ||
    function(d, b) {
        for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p] // [1]
        function __() {
            this.constructor = d
        }
        __.prototype = b.prototype
        d.prototype = new __() // [2]
    }
  1. 기본 클래스의 정적 멤버를 하위 클래스로 복사
  2. 자식 클래스 함수의 프로토타입을 부모의 __proto__로 세팅
  • __proto__: property 검색 시 obj.property에 속성이 없는 경우 obj.__proto__.property에서 검색.
  • 모든 function은 prototype, constructor property를 가짐.
    • new를 이용해 객체를 생성할 경우 새로 생성된 객체의 __proto__ 에 함수의 prototype 할당.

super 키워드는 __proto__를 뒤지는 것이며, 정적 멤버는 클래스에, 상수 멤버는 각 객체에 할당되므로 __proto__에는 메서드, accessor 등만 포함됨.

타입

타입은 값들의 집합.

공집합 - never 타입: 아무런 값도 할당할 수 없음.
단일 값만 포함하는 집합- 리터럴 타입: 입력된 그대로의 값.(=유닛 타입)
합집합 - 유니온 타입.
교집합 - 인터섹션 타입.

A는 B에 할당 가능하다의 의미: A는 B의 부분집합이다.

타입 시스템에서의 변성(Variance)

subtyping: 상위 타입의 객체를 하위 타입의 객체로 치환할 수 있음.(*리스코프 치환 원칙)
Variance: 더 복잡한 타입들끼리의 subtyping이 등장했을 때 어떻게 동작해야 하는가?

  • List<Cat>과 List<Animal>
  • () => Cat과 () => Animal

변성의 종류

  • 공변형(Covariant): 서브타입의 순서가 보존됨. 좁은 타입 <= 넓은 타입.
  • 반변형(Contravariant): 서브타입의 순서가 반대로 보존. 넓은 타입 <= 좁은 타입.
  • 양변형(Bivariant): 넓은 타입으로도, 좁은 타입으로도 변환 가능.
  • 불변형(Invariant): 타입을 변환할 수 없음.

타입 스크립트에서의 변성

기본적으로 Covariant.

let array1: Array<string | number> = [];
let array2: Array<string> = [];
array1 = array2; // Ok
array2 = array1; // Error

let func1: (x: string) => number | string = (x) => 0
let func2: (x: string) => number = (x) => 0
func1 = func2 // Ok
func2 = func1 // Error

함수의 인수는 Contraviariant.

let func1: (x: string | number) => number = (x) => 0
let func2: (x: string) => number = (x) => 0
func1 = func2 // Error
func2 = func1 // Ok
  • 더 많은 종류를 처리할 수 있는 함수를 더 적은 종류를 처리할 수 있는 함수로서 사용하는 것은 아무 문제가 없음.

메서드 Shorthand Definition으로 정의하면 메서드 인자에 대해 Bivariant.

  • Method shorthand definition: ES6에서 메서드를 정의하기 위해 추가된 문법.
let o = {
    func1: function(item: string | number): void {}, // normal
    func2: (item: string | number) => void {}, // arrow
    func3(item: string | number): void {}, // method
}

const func = (item: string): void => {}
o = {
    func1: func, // Error
    func2: func, // Error
    func3: func // Ok
}
  • 이 때문에 Array 등이 공변적으로 동작할 수 있음.

타입 공간 vs 값 공간

타입 공간: 타입 주석으로 사용할 수 있는 것들.
값 공간: 변수로 사용할 수 있는 항목들.

interface Person {
  first: string;
  last: string;
}
const p: Person = {first: 'Jane', last: 'Jacos' };

interface Person: 타입으로 사용됨.
const p: 값으로 사용됨.

타입 공간에 있는 것들은 타입 체커에서만 사용되며, 런타임에는 아무 영향도 끼치지 않음(컴파일 시 제거됨).

interface Cylinder {
  radius: number;
  height: number;
}
const Cylinder = (radius: number, height: number) => ({radius, height});
  • interface Cylinder는 타입으로 사용되며, const Cylinder는 값으로 사용됨.
  • interface는 타입 공간에서만 사용되므로 타입 체킹 외에 런타임에서 interface 값을 직접 사용할 수 없음.
  • class와 enum은 타입과 값 두 가지가 모두 가능.

typeof 연산자와 같이 타입에서 쓰일 때와 값으로 쓰일 때 다른 역할을 하는 연산자가 있음.

const v = typeof Cylinder; // 값이 "function"
type T = typeof Cylinder; // 타입이 "typeof Cylinder"

null / undefined

각각 null과 undefined라는 하나의 값만을 갖는 타입. null와 undefined는 기본적으로 모든 타입의 서브타입이지만, --strictNullChecks 플래그 활성화 시 다른 타입에 null / undefined를 할당할 수 없음.

null / undefined를 비교할 때 == null과 같이 동등연산자를 사용하여 비교하는 것을 추천.(undefined도 함께 걸러짐)
단, 최상단에서 undefined를 확인할 시 해당 값이 정의되지 않았다면 직접 참조 시 Reference Error 발생

declare const someglobal: number

if (typeof someglobal !== 'undefined') {
    // 전역에서 사용해도 안전
    console.log(someglobal)
}

any를 현명하게 사용하기

any의 사용 범위 최소한으로 줄이기.

function f1() {
  const x: any = expressionReturningFoo();
  processBar(x);
}

function f2() {
  const x = expressionReturningFoo();
  processBar(x as any);
}
  • f2는 any 타입이 processBar에서만 사용되었으므로 더 나음.

any를 최대한 구체적으로 작성하기.(any보다는 any[])

타입 단언문을 잘 작성된 함수의 타입 안에 감추기.

function cacheLast<T extends Function>(fn: T): T {
  let lastArgs: any[] | null = null;
  let lastResult: any;
  return function(...args: any[]) {
    if (!lastArgs || !shallowEqual(lastArgs, args)) {
      lastResult = fn(...args);
      lastArgs = args;
    }
    return lastResult;
  } as unknown as T;
}
  • 함수 밖으로 잘못 쓰인 any 타입이 방출될 일이 없으므로 괜찮음.

unknown

any를 사용할 경우 타입 체커를 이용하기 어려움. 때문에 unknown을 사용.

  • 어떠한 타입이든 unknown에 할당 가능.
  • unknown은 오직 unknown과 any에만 할당 가능.
  • unknown 타입인 채로 값을 사용하면 오류 발생.

개발자가 적절한 타입으로 변환하도록 강제하기.

interface Feature { 
  id?: string | number;
  geometry: Geometry;
  properties: unknown;
}
  • properties는 원하는 타입으로 꼭 단언해주어야 함.

이중 표명 리팩토링 시 any보다 안전.

declare const foo: Foo;
let barAny = foo as any as Bar;
let barUnk = foo as unknown as Bar;
  • barUnk는 분리되는 즉시 오류를 발생시키므로 단언문 분리 리팩토링 시 안전함.

union type vs enum vs const object

// union type
type DeviceType = 'phone' | 'desktop' | 'pad' | 'watch'

// enum
enum DeviceType {
    phone = 'phone',
    desktop = 'desktop',
    tablet = 'tablet',
    watch = 'watch'
}

// const object
const DeviceTypeObject = {
    phone: 'phone',
    desktop: 'desktop',
    tablet: 'tablet',
    watch: 'watch'
} as const
type DeviceType = typeof DeviceTypeObject[keyof typeof DeviceTypeObject]

enum의 단점

  • 확장이 불가능.

  • enum을 사용하기 위해서는 import가 필요.

    enum Fruit {
        Apple = 'Apple',
        Banana = 'Banana
    }
    
    getSomeFruits('Apple') // X
    getSomeFruits(Fruit.Apple) // O
  • enum 중 numeric enum은 타입 안정성이 보장되지 않음.

    enum Status {
        pending = 0,
        success = 1,
        fail = 2
    }
    
    const newStatus: Status = 100 // 에러 발생하지 않음!
    • numeric enum으로 flag 값을 정의하고, bitwise 연산을 통해 값을 다루는 것을 고려.
    • 때문에 flag로 사용하지 않는다면 numeric enum 사용을 지양하는 것이 안전.
  • 컴파일된 코드 사이즈가 가장 큼.

    • enum을 const로 선언할 경우 모든 enum 사용을 인라인으로 변환하여 크기가 작아짐.

Best Practice
1. 우선 union type으로 타입 정의.
2. 런타임에 접근이 필요하다면 const object 사용.
3. numeric enum에서의 리버스 매핑, flag 값 정의 및 bitwise 연산 사용 등 특별한 경우에 한하여 enum 사용.

type alias vs interface

type Tstate = {
  name: string;
  capital: string;
}

interface IState {
  name: string;
  capital: string;
}

공통점

  • 추가 속성과 함께 할당하면 오류 발생.
    const wyoming: TState = {
      name: 'Wyoming',
      capital: 'Cheyenne',
      population: 500_000
    };
  • 인덱스 시그니처 사용 가능.
    type TDict = { [key: string]: string};
    interface IDict {
      [key: string]: string;
    }
  • 함수 타입 지정 가능.
    type TFn = (x: number) => string;
    interface IFn {
      (x: number) :string;
    }
  • 제네릭 사용 가능.
    type TPair<T> = {
      first: T;
      second: T;
    }
    interface IPair<T> {
      first: T;
      second: T;
    }
  • 클래스 구현 시 사용 가능.
    class StateT implements TState {
      name: string = '';
      capital: string = '';
    }
    class StateI implements IState {
      name: string = '';
      capital: string = '';
    }

차이점

  • 선언 병합: 인터페이스는 타입 확장 가능. 개방 폐쇄 원칙에 따라 확장에 열려 있음.
    interface IState {
      name: string;
      capital: string;
    }
    interface IState {
      population: number;
    }
    const wyoming: IState = {
      name: 'Wyoming',
      capital: 'Cheyenne',
      population: 500_000
    };
  • 합 타입: extends, implements는 정적으로 알 수 있는 객체 타입/곱 타입에만 사용할 수 있으므로 합 타입을 표현하기 위해서는 타입 별칭 사용.
  • 튜플: 타입 별칭으로 훨씬 간결하게 표현할 수 있으며, concat과 같은 tuple 메서드도 사용 가능.
    type Pair = [number, number];
    interface Tuple {
      0: number;
      1: number;
      length: 2;
    }

Best Practice
1. 우선 interface 사용.(특히 외부 API일 경우)
2. 합 타입, tuple 사용 시 타입 별칭 사용.

타입 추론

타입을 명시적으로 선언하지 않아도 타입을 알 수 있다면 타입이 추론됨. 이를 이용한다면 리팩토링이 쉬워짐.

타입 추론이 되더라도 명시적으로 선언하는 게 나은 경우.

  • 객체 리터럴 정의
    const elmo: Product = {
      name: 'Tickle Me Elmo',
      id: '048188 627152',
      price: 28.99,
    };
    • 잉여 속성 체크를 통해
      1. 선택적 속성이 있는 타입의 오타 같은 오류를 잡는 데 효과적.
      2. 변수가 사용되는 순간이 아닌, 할당하는 시점에 오류를 발생.
  • 함수의 반환: 추론이 가능할지라도 구현상의 오류가 함수를 호출한 곳 까지 영향을 미치게 하지 않기 위하여 타입 구문을 명시하는 것이 좋음.

Best Practice

  • 함수 내에서 생성된 지역 변수에는 타입 구문을 넣지 않아야 깔끔하게 작성.
  • 객체 리터럴 정의, 함수 반환에서는 타입이 추론되더라도 타입 선언.

타입 넓히기

타입을 추론할 경우 값을 가지고 할당 가능한 값의 집합을 유추함.
필요하다면 타입 넓히기 과정을 제어할 필요가 있음.

  • const 변수로 선언: const로 선언 시 가장 좁은 리터럴 타입으로 추론.
  • const 단언문 사용: const 단언을 할 경우 가장 좁은 리터럴 타입으로 추론.
    • as const: 해당 값의 내부까지도 상수(readonly)라는 사실을 타입스크립트에게 안내
const v1 = {
  x: 1,
  y: 2,
}; // { x: number; y: number; }
const v2 = {
  x: 1 as const,
  y: 2m
}; // { x:1 , y: number; }
const v3 = {
  x: 1,
  y: 2,
} as const; // { readonly x: 1; readonly y: 2}

const a1 = [1, 2, 3]; // number[]
const a2 = [1, 2, 3] as const; // readonly [1, 2, 3]

타입 좁히기, 타입 가드

조건문을 이용해 타입 가드를 활용한다면 넓은 타입에서 좁은 타입으로 타입을 좁혀가며 추론할 수 있음.

  • null 체크
  • typeof
  • instanceof
  • 속성 체크
class Address {
    constructor(public city: string, public zip: string) {}
}

function print(name: string | null, address: Address | {}) {
    if (name != null) {
        console.log(name)
    } else {
        console.log("name is null")
    }

    if (typeof name == 'string') {
        console.log(name)
    } else {
        console.log("name is null")
    }

    if (address instanceof Address) {
        console.log(address.city + " " + address.zip)
    } else {
        console.log("address is null")
    }

    if ('city' in address) {
        console.log(address.city + " " + address.zip)
    } else {
        console.log("address is null")
    }
}

구별된 유니온: 공통된 리터럴 멤버 속성이 있다면 해당 속성으로 유니온 구성원을 구별할 수 있음.

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
type Shape = Square | Rectangle;

function area(s: Shape) {
    if (s.kind === "square") {
        return s.size * s.size;
    }
    else {
        return s.width * s.height;
    }
}

사용자 정의 타입 가드: 어떠한 인자는 어떠한 타입이다, 라는 값을 반환하는 함수.

function isDefined<T>(x: T | undefined): x is T {
  return x !== undefined;
}
const members = ['Janet', 'Michael'].map(
  who => jackson5.find(n => n === who)
).filter(isDefined); // string[]

인덱스 시그니처

동적 데이터 등을 작성할 때, 해당 데이터의 형식을 정의.

let foo:{ [index:string] : {message: string} } = {};
foo['a'] = { message: 'some message' }; // Ok
foo['a'] = { messages: 'some message' }; // Error: Type '{ messages: string; }' is not assignable to type '{ message: string; }'.`message` 부분에 오타 존재

인덱서의 key 타입은 string, number, symbol 혹은 template literal type만 사용 가능.

  • string 외의 다른 타입도 JS 코드로 컴파일 시 내부적으로 string으로 변환됨.
  • 위의 내용으로 인해 string 인덱서가 다른 인덱서와 함께 있을 경우 다른 인덱서는 string 인덱서의 일부만 수용하여야 함.

인덱스 시그니처 사용 시 주의할 점

  • 문자열 인덱서와 유효한 값을 섞어 쓰지 말 것: 오타가 발생해도 검출되지 않음.
interface NestedCSS {
  color?: string;
  [selector: string]: string | NestedCSS | undefined;
}

const failsSilently: NestedCSS = {
  colour: 'red', // `colour`는 유효한 문자열 인덱서이므로 오류 아님
}
  • 키마다 다른 타입을 가질 수 없음.

Mapped object type: string, number, symbol 등의 타입 외 별도의 타입을 key로 사용하고 싶거나, 키마다 별도의 타입을 사용하고 싶다면?

type Vec3D = {[k in 'x' | 'y' | 'z' ]: number};
type ABC = {[k in 'a' | 'b' | 'c' ]: k extends 'b' ? string : number};

함수

함수 선언

함수 선언의 두 가지 방식

  • 콜러블, 화살표 구문
type Callable = {
    (a: number): number;
};

type ArrowFunction = (a: number) => number;

두 방식은 동일하나, 오버로드 추가는 콜러블 방식에서만 가능.

type OverloadCallable = {
    (a: number): number;
    (a: string): string;
};

뉴어블: 특별한 종류의 콜러블 타입 어노테이션. 앞에 new가 붙으며, 실행 시 new를 통해 실행시켜야 함.

interface CallMeWithNewToGetString {
  new(): string
}

declare const Foo: CallMeWithNewToGetString;
const bar = new Foo();

오버로딩

타입스크립트에서 함수 오버로드는 타입 체커에게 인자를 알려주기 위함. 이는 런타임에서 사라지므로 런타임 동작과는 상관 없음.
즉 실제로 JS 코드로 컴파일되는 함수 바디 내에서 모든 인자 타입에 따른 케이스를 구현해야 함.

// Overloads
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
function padding(a: number, b?: number, c?: number, d?: number) {
    if (b === undefined && c === undefined && d === undefined) {
        b = c = d = a;
    }
    else if (c === undefined && d === undefined) {
        c = a;
        d = b;
    }
    return {
        top: a,
        right: b,
        bottom: c,
        left: d
    };
}

오버로딩 타입보다 조건부 타입 사용하기

조건부 타입을 이용하여 타입 공간에 if문을 만들어주는 효과를 줄 수 있음.

function double1(x: number|string): number|string;
function double1(x: any) { return x + x }

const num1 = double1(12) // string | number
const str1 = double1('x') // string | number
  • 유니온 타입을 사용할 경우 반환 타입 또한 유니온 타입.
function double2(x: number): number;
function double2(x: string): string;
function double2(x: any) { return x + x };

const num2 = double2(12); // number
const str2 = double2('x'); // string

function f2(x: number|string){
  return double2(x); // string|number 형식의 인수는 'string'형식의 매개변수에 할당 불가
}
  • 오버로딩을 사용할 경우 반환 타입은 인자에 따라 잘 바뀌나, 유니온 타입을 인자로 받을 수 없음.
function double3<T extends number | string> (
 	x: T
): T extends string ? string: number;
function double3(x: any) { return x + x };

const num3 = double2(12); // number
const str3 = double2('x'); // string

function f3(x: number|string){
  return double3(x); // string|number 형식의 인수는 'string'형식의 매개변수에 할당 불가
}
  • 조건부 타입을 이용하면 유니온 타입을 인자로 받아 인자에 따른 반환 타입을 바꿔줄 수 있음.

This 타입

함수 내부의 this 값은 함수가 정의되는 시점이 아닌, 실행되는 시점에 결정되므로 this 타입을 추론하기 어려움. 이를 위해 함수 내에서 this 타입을 명시할 수 있음.

class C {
    a: string = "a"

    f(this: C) {
        console.log(this.a)
    }
}

const c: C = new C()
c.f()
const callback = c.f
callback() // Error: The 'this' context of type 'void' is not assignable to method's 'this' of type 'C'.

Decorator

Decorator: 클래스의 다양한 property 및 method의 정의를 수정 및 교체하는 function.

  • 클래스 및 클래스 내부에서만 사용할 수 있음.(class, property, accessor, method, parameter)
  • runtime에 호출됨. 즉, class instance를 생성하지 않아도 호출됨.

클래스 데코레이터

일부 속성이나 메서드로 기존 클래스를 확장하는 데 적합.

type ClassDecorator = <TFunction extends Function>
  (target: TFunction) => TFunction | void;
  • params
    • target: 클래스 생성자.
  • returns: 반환한 값이 클래스 선언을 대체.

속성 데코레이터

정보 수집 혹은 일부 메서드나 속성 추가까지도 가능.

type PropertyDecorator =
  (target: Object, propertyKey: string | symbol) => void;
  • params
    • target: 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입.
    • propertyKey: 속성의 이름.

메서드 데코레이터, 접근자 데코레이터

메서드 데코레이터와 접근자 데코레이터는 거의 유사함. 유일한 차이는 속성 설명자의 프로퍼티.
설명자를 통해, 원래 구현을 재정의할 수 있음.

type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
  • params
    • target: 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입.
    • propertyKey: 멤버의 이름.
    • descriptor: 멤버의 속성 설명자.
interface PropertyDescriptor {
  configurable?: boolean;
  enumerable?: boolean;
  value?: any;
  writable?: boolean;
  get?(): any;
  set?(v: any): void;
}
  • 공통으로 갖는 속성
    • enumerable: for-in, Object.keys로 속성 열거 시 드러나는지 여부.
    • configurable: 속성의 정의를 수정할 수 있는지 여부.
  • 메서드 데코레이터의 설명자만 갖는 속성
    • value: 현재 값.
  • 접근자 데코레이터의 설명자만 갖는 속성
    • get: getter
    • set: setter

파라미터 데코레이터

일반적으로 다른 데코레이터가 사용할 수 있는 정보를 기록하는데에 사용.

type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;
  • params:
    • target: 정적 멤버에 대한 클래스의 생성자 함수 또는 인스턴스 멤버에 대한 클래스의 프로토타입.
    • propertyKey: 파라미터가 사용되는 메서드 명.
    • parameterIndex: 매개변수의 서수 인덱스.

Metadata

reflect-metadata 라이브러리를 통해 각종 타입에 대한 metadata를 정의할 수 있음.

  • Reflect.defineMetadata: 메타데이터 프로퍼티 추가. getMetadata로만 가져올 수 있음.
  • Reflect.getMetadata: defineMetadat로 추가된 메타데이터 프로퍼티를 읽어옴.
import "reflect-metadata"

const user = {
  firstName: "정민",
}

Reflect.defineMetadata("phone", "010-1234-5678", user)
console.log(user)

const number = Reflect.getMetadata("phone", user)
console.log(number)

모듈

import / export

require / exports: CommonJS 키워드

  • 파일에 들어가있는 곳에 남아있음.
  • 프로그램의 어느 지점에서나 호출 가능.

import / export: ES6 키워드

  • 맨 위로 이동
  • 파일의 시작 부분에서만 실행 가능.
  • 필요한 모듈 부분만 선택하고 로드할 수 있음.
  • require보다 성능이 우수하고 메모리가 절약됨.

변수, 상수, 함수, 클래스: JS 모듈 로더 코드로 컴파일.
타입: JS코드에서는 삭제됨.
네임스페이스: JS 일반 객체로 컴파일.(IIFE 함수에 해당 객체를 전달하고, 그 함수를 즉시 호출하는 식.)

default로 모듈을 export할 시 import하면 모듈명.객체명으로 접근.
각각 export할 시 각 객체들을 import 해야 함.

declare / .d.ts

declare: 명시된 변수, 상수, 함수, 클래스가 어딘가에 이미 선언되어 있음을 알림. 즉, TS 컴파일러에게 타입 정보를 알리기만 함.
declare 블록(declare namespace, declare module, declare global)

  • ambient context로 정의되며, 이 영역 안에서는 declare 키워드가 기본으로 붙음. 일반 코드를 작성할 수 없고, 선언 코드만 작성할 수 있음.
  • declare namespace: JS 코드로 컴파일되지 않음. 몇몇 타입들을 의미적으로 묶고 싶은 경우에 사용.
  • declare module: 컴파일 대상에 포함될 경우 그곳에 선언된 모듈의 타입 정보를 참조.
  • declare global; 모듈 파일에서도 전역 참조가 가능한 선언 코드를 작성하고 싶을 경우.(=별도로 불러오지 않아도 참조할 수 있음.)
    • declare module 블록 안에서만 중첩 가능.

.d.ts파일: 선언부만을 작성하는 용도의 파일. 즉 JS 코드로 컴파일되지 않음. 이 파일에 작성되는 declare namespace 블록과 declare module 블록의 필드들에는 export 키워드가 기본으로 붙음.

  • export as namespace: .d.ts 파일에서만 사용할 수 있는 문법. export 되는 데이터들을 하나의 네임스페이스로 묶어서 export 시키며, 이 네임스페이스는 불러오기 코드 없이 전역 참조 가능.

참고자료

profile
중니어 개발자

0개의 댓글