221025 TIL | 이펙티브 타입스크립트_#6 타입 연산과 제너릭 사용으로 반복 줄이기

아이템14 까지 정리

키워드

  • DRY 원칙을 타입에 적용하기
  • extends를 사용하여 인터페이스 필드 반복 줄이기
  • 타입스크립트가 제공하는 도구 사용 : keyof, typeof, 인덱싱, 매핑된 타입
  • 제네릭 타입 이용하기
  • 제네릭 타입을 이용할 때는 타입 제한을 위해 extends 사용하기
  • 표준 라이브러리에 정의된 Pick, Partial, ReturnType 제네릭 타입 알기

타입 반복 줄이기

  • 같은 코드를 반복하지 말라는 DRY(don’t repeat yourself)원칙은 타입에도 똑같이 적용된다.
  • 타입의 반복을 줄이는 여러가지 방법을 알아보자.

타입에 이름을 붙이기

반복 줄이는 작업 적용 전

function distance(a: {x: number, y: number}, b: {x: number, y: number}) {
  return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}

적용 후

interface Point2D {
  x: number;
  y: number;
}
function distance(a: Point2D, b: Point2D) { /* ... */ }

함수 시그니처를 명명된 타입으로 분리하기

  • 변경 전
function get(url: string, opts: Options): Promise<Response> { /* COMPRESS */ return Promise.resolve(new Response()); /* END */ }
function post(url: string, opts: Options): Promise<Response> { /* COMPRESS */ return Promise.resolve(new Response()); /* END */ }
  • 적용 후
// HIDE
interface Options {}
// END
type HTTPFunction = (url: string, options: Options) => Promise<Response>;
const get: HTTPFunction = (url, options) => { /* COMPRESS */ return Promise.resolve(new Response()); /* END */ };
const post: HTTPFunction = (url, options) => { /* COMPRESS */ return Promise.resolve(new Response()); /* END */ };

존재하는 타입을 확장하고 싶을 때

  • 이미 존재하는 타입을 확장하는 경우에는 인터섹션 연산자(&)를 쓸 수 있다.
    • 확장할 수 없는 유니온 타입에 속성을 추가하려고 할 때 유용하다.
interface Person {
  firstName: string;
  lastName: string;
}
type PersonWithBirthDate = Person & { birth: Date };

Pick이란?

  • 다음과 같은 예제가 있을 때 TopNavState를 확장하여 State를 구성하는 것보다 부분 집합으로 TopNavState를 정의하는 것이 바람직 해보인다.
interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}
interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
}
  • 부분집합으로 정의하는 방법에는…
    1. TopNavState를 인덱싱하여 속성의 타입에서 중복을 제거하는 방법
      type TopNavState = {
        userId: State['userId'];
        pageTitle: State['pageTitle'];
        recentFiles: State['recentFiles'];
      };
    
  1. 매핑된 타입을 사용하는 방법이 있다.

     interface State {
       userId: string;
       pageTitle: string;
       recentFiles: string[];
       pageContents: string;
     }
     type TopNavState = {
       [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
     };
    
  • 여기서 매핑된 타입은 배열의 필드를 루프 도는 것과 같은 방식이다.
  • 이 패턴은 표준 라이브러리에서도 찾을 수 있으며 ⭐ Pick이라고 한다.
// 적용 예제
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
  • Pick은 제네릭 타입이다.
  • Pick은 함수처럼 작동하는데 T와 K 두 가지 타입을 받아 결과 타입을 반환한다.
interface SaveAction {
  type: 'save';
  // ...
}
interface LoadAction {
  type: 'load';
  // ...
}
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load';  // 타입 반복!
  • 이 예제에서 Action 유니온을 인덱싱하면 타입 반복 없이 ActionType을 정의할 수 있다.
    • Action 유니온에 타입을 더 추가하면 ActionType 또한 자동적으로 그 타입을 포함하게 된다.
type ActionType = Action['type'];  // Type is "save" | "load"
  • 해당 방법은 Pick을 사용해서 얻게 되는 것과 다르다.
    • Pick으로 얻은 결과는 type 속성을 가지는 인터페이스다 ← ??
    • 타입과 타입 속성을 가지는 인터페이스가 다른 건가…??
type ActionRec = Pick<Action, 'type'>;  // {type: "save" | "load"}

“매핑된 타입”과 “keyof”를 사용하여 생성 후 업데이트가 되는 클래스 만들기

  • 에시 속 OptionsUpdate는 대부분이 선택적 필드이다.
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
interface OptionsUpdate {
  width?: number;
  height?: number;
  color?: string;
  label?: string;
}
class UIWidget {
  constructor(init: Options) { /* ... */ }
  update(options: OptionsUpdate) { /* ... */ }
}
  • 이때 매핑된 타입과 keyof를 사용하면 Options로부터 OptionsUpdates를 만들 수 있다.
type OptionsUpdate = {[k in keyof Options]?: Options[k]};
  1. keyof타입을 받아서 속성 타입의 유니온을 반환한다.

     interface Options {
       width: number;
       height: number;
       color: string;
       label: string;
     }
     type OptionsKeys = keyof Options;
     // 타입이 "width" | "height" | "color" | "label"
    
  2. 매핑된 타입(k in keyof Options)은 순회하며 Options 내 k 값에 해당하는 속성이 있는지 찾는다.
  3. ?는 각 속성을 선택적으로 만든다.

→ 아주 일반적인 패턴이며, 표준 라이브러리에도 Partial이라는 이름으로 표함되어 있다.

// Partial 예시
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
class UIWidget {
  constructor(init: Options) { /* ... */ }
  update(options: Partial<Options>) { /* ... */ }
}

“typeof”를 이용하여 값의 형태에 해당하는 타입 정의하기

const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: '#00FF00',
  label: 'VGA',
};

// 반복적으로 인터페이스를 정의하는 대신에..
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}

// typeof를 사용한다.
type Options = typeof INIT_OPTIONS;
  • 예시에서 사용한 typeof는 자바스크립트의 런타임 typeof가 아닌 타입 스크립트의 함수이다.
  • typeof는 정확하게 타입을 표현한다.
  • typeof 사용 시 타입 정의를 먼저 한 후, 해당 타입이 할당 가능하다고 선언하는 편이 좋다.
    • 타입이 명확해지고, 예상하기 어려운 타입 변동을 방지할 수 있다.

함수나 메서드의 반환값에 명명된 타입을 만들고 싶을 때 사용하는 “ReturnType 제네릭”

function getUserInfo(userId: string) {
  // COMPRESS
  const name = 'Bob';
  const age = 12;
  const height = 48;
  const weight = 70;
  const favoriteColor = 'blue';
  // END
  return {
    userId,
    name,
    age,
    height,
    weight,
    favoriteColor,
  };
}
// Return type inferred as { userId: string; name: string; age: number, ... }

type UserInfo = ReturnType<typeof getUserInfo>;
  • ReturnType은 함수의 값인 getUserInfo가 아니라 함수의 타입인 get UseInfo에 적용된다.

제네릭 타입에서 매개변수를 제한하려면 “extends” 사용

  • 제네릭 타입은 타입을 위한 함수와 같다.
  • 함수에서 매개변수로 매핑할 수 있는 값을 제한하기 위해 타입 시스템을 사용하는 것처럼, 제너릭 타입에서도 매개변수를 제한할 수 있는 방법이 필요하다.
  • extends를 사용하여 제네릭 매개변수가 특정 타입만 확장한다고 선언할 수 있다.
interface Name {
  first: string;
  last: string;
}
type DancingDuo<T extends Name> = [T, T];

const couple1: DancingDuo<Name> = [
  {first: 'Fred', last: 'Astaire'},
  {first: 'Ginger', last: 'Rogers'}
];  // OK
const couple2: DancingDuo<{first: string}> = [
                       // ~~~~~~~~~~~~~~~
                       // Name 타입에 필요한 'last' 속성이
				               // '{ first: string; }' 타입에 없다
  {first: 'Sonny'},
  {first: 'Cher'}
];
  • {first: string}은 Name을 확장하지 않기 위해 오류가 발생한다.

extends를 이용하여 Pick의 사용방법 예제를 완성하기

  • K는 인덱스로 사용될 수 있는 string | number | symbol이 되어야 하며, 실제로는 범위를 좁혀서 사용해야 한다.
  • K는 T의 부분 집합, 즉 keyof T가 되어야 한다.
// Pick 사용방법
type Pick<T, K extends keyof T> = { 
	[k in K]: T[k] 
}

출처

  • 이펙티브 타입스크립트