Programming/TypeScript

[TypeScript] 타입 연산과 제너릭 사용으로 반복 줄이기

앵도라지 2023. 4. 22. 23:16

이 글은 이펙티브 타입스크립트 '아이템 14 타입 연산과 제너릭 사용으로 반복 줄이기'를 정리한 글입니다.

 

 

  • 코드를 짤 때는 같음 코드를 반복하지 말라는 DRY (don’t repeat yourself) 원칙이 있다.
  • 타입에서도 같은 원칙을 적용해야 한다.
    • 타입 중복은 많은 문제를 발생시킨다.

 

방법 1 : 타입에 이름 붙이기

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) {
  /* ... */
}



방법 2 : 같은 시그니처를 공유하고 있는 함수는 명명된 타입으로 분리해 반복 줄이기

//같은 시그니처 공유
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 */
}

//명명 타입으로 분리
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 */
}



방법3 : 한 인터페이스가 다른 인테페이스를 확장

interface Person {
  firstName: string
  lastName: string
}

interface PersonWithBirthDate extends Person {
  birth: Date
}



방법 4 : 이미 존재하는 타입을 확장하는 경우에는 인터섹션 연산자(&) 사용

interface Person {
  firstName: string
  lastName: string
}

type PersonWithBirthDate = Person & { birth: Date }



방법 5 : State 인덱싱 → 중복 제거

//방법 5, 6, 공통 예제
//전체 상태를 표현하는 State 타입과 부분만 표현하는 TopNavState가 있는 경우

interface State {
  userId: string
  pageTitle: string
  recentFiles: string[]
  pageContents: string
}
interface TopNavState {
  userId: string
  pageTitle: string
  recentFiles: string[]
}
type TopNavState = {
  userId: State['userId']
  pageTitle: State['pageTitle']
  recentFiles: State['recentFiles']
}



방법 6 : 매핑된 타입을 사용

type TopNavState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
}
  • 매핑된 타입
    • 매핑된 타입은 in 키워드를 사용해 배열이나 튜플 등의 타입에 대해 루프를 도는 것과 같은 방식으로 새로운 타입을 만들어내는 기능이고, 주로 객체나 배열의 타입변환에 사용된다.
    • 이 패턴은 표준 라이브러리에서도 일반적으로 찾을 수 있으며, Pick 이라고 한다.
  • Pick
    • Pick 은 제네릭 타입으로 첫번째 인자로 객체타입을, 두번째 인자로 해당 객체 타입에서 추출하고자 하는 필드의 이름들을 문자열 리터럴 타입 배열로 받는다.
    • Pick 을 사용해서 State 에서 userId, pageTtiel, recentFiles 필드만 추출해서 간단하게 새로운 타입을 만들 수 있다.
type Pick<T, K> = { [k in K]: T[k] };



방법 7 : 유니온 인덱싱

  • 중복 발생 : 태그된 유니온에서도 다른 형태의 중복이 발생할 수 있다.
  • 태그된 유니온 : 유니온 타입에 문자열 리터럴 타입을 추가해서 타입 안전성을 높인 방법
interface SaveAction {
  type: 'save'
  // ...
}
interface LoadAction {
  type: 'load'
  // ...
}
type Action = SaveAction | LoadAction
type ActionType = 'save' | 'load' // Repeated types!
//Action 유니온 인덱싱을 하면 타입 반복없이 ActionType 을 정의할 수 있다
type ActionType = Action['type'] // Type is "save" | "load"
type ActionRec = Pick<Action, 'type'>; //{type: "save" | "load"}



방법 8 : keyof

  • keyof 는 타입을 받아서 속성 타입의 유니온을 반환한다
interface Options{
    width: number;
    height: number;
    ocolor: string;
    label: string;
}

interface OptionsUpdate{
    width?: number;
    height?: number;
    color?: string;
    label?: string;
}

type OptionsUpdate = {[k in keyof Options]?: Options[k]};



방법 9 : typeof

  • 값의 형태에 해당하는 타입을 정의하고 싶을 때 사용
//사용 전
const INIT_OPTIONS = {
    width: 640,
    height: 480,
    color: '#00FF00',
    label: 'VGA',
};

interface Options{
    width: number,
    height: number,
    color: string,
    label: string,   
}

//사용 후
type Options = typeof INIT_OPTIONS



방법 10 : ReturnType

  • 함수나 메서드의 반환값에 명명된 타입을 만들고 싶을 수 있다.
  • ReturnType 을 사용해 함수의 반환 타입을 변수로 선언하거나 다른 타입의 매개변수로 전달할 수 있다.
function getUserInfo(userId: string){
    //...
    return {
        userId,
        name,
        age,
        height,
        weight,
        favoriteColor
    };
}

type UserInfo = ReturnType<typeof getUserInfo>;
  • 위 코드에서 ReturnType 은 함수의 타입인 typeof getUserInfo 에 적용되었다.
  • 적용대상이 값인지 타입인지 정확하게 알고 구분해서 처리해야 한다.

 

 

방법 11 : 제너릭타입

  • 제너릭 타입은 타입을 위한 함수와 같습니다.
  • 제네릭 타입은 타입스크립트에서 DRY 원칙을 적용하는 핵심 방법 중 하나다.
  • 제너릭을 사용하면 타입의 중복을 최소화하고 재사용성을 높일 수 있다.
function addNumbers(a: number, b: number): number {
  return a + b;
}

function concatenateStrings(a: string, b: string): string {
  return a + b;
}

//제너릭으로 변경
function combine<T>(a: T, b: T): T {
  return a + b;
}
  • combine 함수는 T 라는 타입의 매개변수를 사용한다.
  • T 는 함수를 호출할 때 전달된 값의 타입으로 결정됩니다. 따라서 이 함수는 어떤 타입의 값을 합쳐서 반환할 수 있다.
  • 제너릭 타입에서는 매개변수를 제한할 수 있는 방법이 필요다. (타입안전성을 보장하기 위해서)
  • 제너릭 타입을 사용하면 여러 종류의 값들을 다룰 수 있는데, 제너릭 타입이 특정한 종류의 값만 다루도록 제한해야 하는 경우가 있다.
  • extends 를 이용하면 제너릭 매개변수가 특정 타입을 확장한다고 선언할 수 있고 이를 통해 매개변수를 제한할 수 있다.
//제네릭 타입 매개변수 T 는 Name을 확장한다. 
//DancingDuo 타입은 T 타입의 배열이고, 요소는 Name 타입 객체로 이뤄져있다.
interface Name{
    first: string;
    last: string;
}

type DancingDuo<T extends Name> = [T,T];



방법 12 : Pick

type Pick<T, K extends keyof T> = {
    [k in K]: T[k];
};
  • Pick 타입은 T 타입에서 K 타입에 포함된 속성만 선택해 새로운 타입을 정의한다.
  • 이 때 in 키워드를 사용한 매핑타입을 이용해서 새로운 객체 타입을 생성한다.

 

 

요약

  • DRY 원칙을 타입에도 최대한 적용해야 한다.
  • 타입에 이름을 붙여서 반복을 피하고, extends 를 사용해서 인터페이스 필드의 반복을 피해야한다.
  • 타입들간의 매핑을 위해 매핑된 타입 keyof,typeof,인덱싱,매핑된 타입들을 공부해야한다.
  • 제너릭 타입은 타입을 위한 함수와 같다. 타입을 반복하는 대신 제너릭 타입을 사용하여 타입들 간에 매핑을 하는 것이 좋다.