Item 14 타입 연산과 제너릭 사용으로 반복 줄이기

Remove Duplicate Types

코드 중복만큼 타입 중복도 많은 문제를 발생시킨다. 타입에 대한 중복을 제거하는 방법은..

  • 타입에 이름 붙이기

  • 인터페이스 확장하기 (interface A extends B)

  • 인터섹션 연산자 사용하기 (type PersonWithBirthDate = Person & { birth: Date })

  • 인덱싱 사용하기 (.. userId: State['userId'] ..)

그 외에도 다양한 방법을 소개한다.

Pick - 타입의 일부만 가져오기

  • 매핑된 타입 사용하기 (type TopNavState = { [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k] })

    • 표준 라이브러리 type Pick<T, K> = { [k in K]: T[k] }

    • type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>

Partial - 선택적으로 만들기

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로부터 OptionsUpdate를 만들 수 있다.

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

✅ 또는 표준 라이브러리 Partial을 사용할 수 있다.

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를 사용한 것처럼 보이지만, 실제로는 타입스크립트 단계에서 연산되며, 훨씬 더 정확하게 타입을 표현한다.

주의사항도 있다. 이렇게 값으로부터 타입을 만들어 낼 때는 선언의 순서에 주의해야 한다. 타입 정의부터 먼저 하고 그 값이 그 타입에 할당이 가능하다고 선언하는 것이 좋다.

ReturnType - 함수나 메서드의 반환 값에 명명된 타입 만들기

function getUserInfo(userId: string) {
  // ...
  return {
    userId,
    name,
    age,
    height,
    weight,
    favoriteColor,
  }
}
// 추론된 반환 타입: { userId: string; name: string; age: number; ... }

이럴 땐 조건부 타입(Item 50)이 필요하다. 하지만 표준 라이브러리에는 이러한 일반적 패턴의 제너릭 타입이 정의되어 있다.

type UserInfo = ReturnType<typeof getUserInfo>

typeof와 마찬가지로 이런 기법은 신중하게 사용해야 한다. 적용 대상이 값인지 타입인지 정확히 알고 구분해 처리해야 한다.

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 }> = {
  // { first: string } => 'Name' 타입에 필요한 'last' 속성이 '{ first: string; }' 타입에 없습니다.
  { first: 'Sonny' },
  { first: 'Cher' },
] // OK

{ first: string }Name을 확장하지 않으므로 오류가 발생한다.

앞에 나온 Pick의 정의는 extends를 사용하여 완성할 수 있다. 타입 체커를 통해 기존 예제를 실행해보면 오류가 발생한다.

type Pick<T, K> = {
  [k in K]: T[k]
  // ~ 'K' 타입은 'string | number | symbol' 타입에 할당할 수 없습니다.
}

KT타입과 무관하고 범위가 너무 넓다. K는 인덱스로 사용할 수 있는 string | number | symbol이 되어야 하며, 실제로는 범위를 좀 더 좁힐 수 있다. K는 실제로 T의 키의 부분 집합, 즉 keyof T가 되어야 한다.

type Pick<T, K extends keyof T> = {
  [k in K]: T[k]
} // OK

Summary

  • DRY(Don't Repeat Yourself) 원칙을 타입에도 최대한 적용하자.

  • 타입에 이름을 붙여서 반복을 피하고, extends를 이용해 인터페이스 필드의 반복을 피하자.

  • 타입들 간의 매핑을 위해 타입스크립트가 제공한 도구들(keyof, typeof, 인덱싱, 매핑된 타입 등)을 이용하자.

  • 제너릭 타입은 타입을 위한 함수와 같다. 타입을 반복하는 대신 제너릭 타입을 사용해 타입들 간 매핑을 사용하고, 제한하기 위해서 extends를 사용하자.

  • 표준 라이브러리에 정의된 Pick, Partial, ReturnType 같은 제너릭 타입에 익숙해지자.

Last updated