[타입시스템 14] 반복 줄이기(타입연산, 제네릭)

2022. 7. 19. 20:35책/이펙티브 타입스크립트

1. 반복에 대하여

  • 일반적인 코드를 작성할 때 반복하지 말라(DRY원칙)는 말이 있듯 타입 또한 반복을 피해야 한다
  • 문제는 타입의 반복을 어떻게 줄여야 할지 감도 안잡힌 다는 것이다(익숙치 않으니까)
  • TS는 반복 문제를 해결하기 위해 다양한 도구를 제공하고 있으니 여기에 익숙해져야 한다

2. 반복 줄이기

2.1 타입에 이름을 붙여라

  • 반복되는 변수, 상수들을 찾아서 뽑아내라
  • 함수가 같은 타입 시그니처를 공유하는지 확인해라
// 2D 거리 계산하기
function getDist(
  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 Vector2D {
  x :number;
  y :number;
}

function getDist2(a :Vector2D, b :Vector2D) {
  return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2))
}

 

2.2 extends를 활용하라

  • 유사한 프로퍼티를 가진 타입이라면 extends를 통한 확장으로 반복 제거가 가능하다
  • 경우에 따라선 인터섹션 연산자(&)를 활용할 수도 있다(흔치는 않지만)

interface Person {
  firstName :string;
  lastNAme :string;
}

interface PersonBirth {
  firstName :string;
  lastName :string;
  birth :Date;
}

// 유사한 프로퍼티를 공유하고 있다면 extends로
// 반복을 줄이면서 적절히 확장해나가자
interface PersionBirth extends Person {
  birth :Date;
}

 

2.3 매핑된 타입을 활용하라

  • A mapped type : 프로퍼티키들을 순회하면서 타입을 생성하는 제네릭 타입
    generic type which uses a union of 
    PropertyKeys (frequently created via a keyof) to iterate through keys to create a type
  • 배열의 필드를 루프 도는 것과 비슷한 방식이다(Array.forEach( val => { ... } );
  • TS 표준 라이브러리엔 매핑된 타입을 쉽게 쓸 수 있도록 제네릭 타입이 마련되어 있다(Pick, Partial, ReturnType, ... )

interface State {
  userId :string;
  pageTitle :string;
  recentFiles :string[];
  pageContents :string;
}

// 이렇게 State에서 필요한 키를 뽑아오는
// 즉, 인덱싱을 활용하는 방법도 있지만
interface NavBarState {
  userId :State['userId'];
  pageTitle :State['pageTitle'];
  recentFiles :State['recentFiles'];
}

// 이렇게 매핑된 타입으로 우아하게 
// 정의할 수도 있다
type NavbarState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles'] :State[k]
};

 

2.4 제네릭 타입(유틸리티 타입)

◎ Pick<T, K> 

// 이렇게 손수 써준 우아한 타입을
type NavbarState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles'] :State[k]
};

// 제네릭 타입으로 더 쉽게 쓸 수 있다
type NavbarState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;

 

◎ Partial<T>

<T>의 모든 프로퍼티를 선택적 키로 삼는 타입을 생성한다

interface Opts {
  width :number;
  height :number;
  color :string;
  label :string;
}

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

// 반응형으로 UI를 건드리는 위젯이 있다고 치자
// 넓이가 변할지, 길이가 변할지, 뭐가 변할지 아무도 몰루
// 즉, 거의 모든 필드가 선택적이 됨
class UIwidget {
  constructor(init :Opts) {
    // ...
  }

  update(opts :UpdateOpts) {
    // ...
  }
}

// 이 방법도 충분히 우아하지만
type UpdateOps = {
  [k in keyof Opts]? :Opts[K];
};

// 이렇게 더 간단히 쓰는것도 가능
type UpdateOps2 = Partial<Opts>

 

◎ ReturnType<T>

함수의 타입을 정의한 <T>에서 함수가 반환하는 값의 타입을 추출한다

function getUserInfo(userId :string) {
  return {
    userId,
    email,
    company,
    role,
  };
}

// 함수가 뱉어내는 '타입'을 추출하고 싶은거니까
// getUserInfo함수에 typeof를 써준다
type UserInfo = ReturnType<typeof getUserInfo>

 

3. 제네릭 타입 주의사항
함수에서 파라미터로 매핑할 수 있는 값을 제한하듯 제네릭 타입에서도 파라미터를 제한할 수 있는 방법이 필요하다. 예를 들어, ReturnType<T>에서 <T>는 string이나 number만 받고싶다면 이를 제한할 적절한 방법을 찾아야 한다는 뜻이다.

그리고 이 때 extends를 활용할 수 있다.

interface Name {
  first :string;
  last :string;
}

type UserNames<T extends Name> = T[];
const userNames :UserNames<Name> = [
  { first: '군', last: '김' }, 
  { first :'군', last: '나' },
  { first :'군', last: '박' },
]