Frontend/Typescript

아이템14. 타입 연산과 제네릭 사용으로 반복 줄이기

rachel_13 2023. 4. 22. 14:20

https://develub.kr/shop_view/?idx=26

타입 중복은 간과하기 쉽지만 코드 중복만큼 많은 문제를 일으키며, 불필요하게 코드 라인을 늘리게 되는 원인 중 하나이기도 하다.

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

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

 

위 예시를 살펴보자. Person 타입과 PersonWithBirthDate 타입은 언뜻 보기에도 굉장히 유사하다.

PersonWithBirthDate를 Person의 확장개념으로 보면 아래와 같이 중복을 제거할 수 있게 된다.

interface PersonWithBirthDate extends Person {
   birth: Date;
}

 

타입 반복을 줄이는 방법

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 distacnce(a: Point2D, b: Point2D) {...}

 

2. 명명된 타입으로 분리하기

function get(url: string, opt: Options): Promise<Response> {...}
function post(url: string, opt: Options): Promise<Response> {...}

2개 이상의 함수가 같은 타입의 시그니처를 공유하고 있다면, 명명된 타입으로 변경해서 중복된 코드를 줄일 수 있다.

type HTTPFunction = (url: string, opt:Options) => Promise<Response>;
const get:HTTpFunction = (url, opt) => {...};
const post:HTTpFunction = (url, opt) => {...};

 

3. 부분집합으로 타입 정의하기

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

이 예제 역시 TopNavState를 확장해서 State로 선언해서 중복을 줄일 수도 있다. 그렇지만, 네이밍을 보면 State가 조금 더 범용적으로 사용되는 범위임을 알 수 있다. 오히려 State의 부분집합으로 TopNavState를 사용할 수 있으면 더 좋겠다는 생각이 든다.

 

이 때는 State를 인덱싱 하여 속성 타입에서 중복을 제거할 수 있다.

방법1)

type TopNavState = {
  userId; State['userId'];
  pageTitle: State['pageTitle'];
  recentFiles: State['recentFiles'];
}

흠... 여전히 변경될 수 있는 요소들이 보인다.

 

매팅된 타입을 이용해서 조금 더 간결하게 작성해보자.

방법2) 

type TopNavState = {
 [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
}

매핑된 타입 => 배열의 필드를 루프도는 것과 동일한 방식이다. 이러한 패턴은 타입스크립트에서 기본으로 제공하고 있으며

Pick을 사용하면 동일한 기능을 구현할 수 있다.

방법3)

type Pick<T, k> = { [k in K]: T[K]}
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;

 

4. 태그된 유니온에서 중복발생 시 

interface SaveAction {
  type: 'save';
}
interface LoadAction {
  type: 'load'
}
type Action = SaveAction | LoadAction;

typeActionType = 'save' | 'load' //타입의 반복

typeActionType = Action['type'] //'save | 'load'

 

5. 매핑된 타입과 keyof를 이용해서 중복 없애기

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

update 매개변수의 타입은 생성자 타입과 동일하면서, 타입이 선택적 필드가 되는 것인데,

이를 매핑 타입과 keyof 를 사용해서 Options로부터 OptionsUpdate를 만들수 있다.

 

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

//keyof Options: 'width' | 'height' | 'color' | 'label';

keyof Options  👉 매핑된 타입, Options를 순회하면서 k 값에 해당하는 속성이 있는지 찾는다.

=> 타입스크립트에서 지원해주는 속성을 활용하자 : Partial!

type Partial<T> = {
    [P in keyof T]?: T[P];
};

Partial을 이용하면 다음과 같이 간결하게 코드를 수정할 수 있다.

type OptionsUpdate = Partial<Options>

 

6. 값의 형태에 해당하는 타입 정의하기

const INIT_OPTIONS = {
    width: 644,
    height: 400,
    color: '#ff00ee',
    label: 'VGA'
}

type Options = typeof INIT_OPTIONS;

/*
type Options = {
    width: number;
    height: number;
    color: string;
    label: string;
}
*/

런타임 되기 전, 타입 체크 단계에서 typeof 연산자가 사용되며 (자바스크립트의 런타임 연산자 typeof 가 아님.) 더 정확한 타입을 표현한다.

 

*유의사항 : 선언 순서에 주의할 것! : (선) 타입정의 → (후) 타입 할당 가능 선언

 

7.  함수or 메서드 반환 값에 명명된 타입 지정하기

ReturnType 활용하기

function getUserInfo(userId: string){
    return {
        userId,
        name,
        age,
        height,
        weight,
        favoriteColor,
    }
}

type UserInfo = ReturnType<typeof getUserInfo>

UserInfo 타입에 getUserInfo 함수의 타입을 ReturnType으로 지정하여 할당하였다.

 

8. 제네릭 타입에서 매개변수 제한하기

extends 사용하기, 제네릭 매개변수가 특정타입을 확장한다고 선언할 수 있다.

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

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

const couple1: DancingDuo<Name> = [
    {first: 'Joy', last: 'Joshua'},
    {first: 'Ginger', last: 'Rogers'}
]

const couple2:DancingDuo<{first: string}> = [
    {first: 'Sony'},
    {first: 'Alex'}
]
//🚨 오류 발생! '{first: string; }' 유형에 'last' 속성이 없습니다

copule2에서 매개변수 {first: string}는 Name 타입을 확장하지 않기 때문에 오류가 발생한다. (Name의 부분집합, 상속개념이 아님)

임의로 ParamsType을 설정해서 오류를 수정해보았다.

type ParamsType = {middle?: string} & Name;
const couple2:DancingDuo<ParamsType> = [
    {first: 'Sony', last: 'Smith'},
    {first: 'Alex', last: 'James', middle: 'Saint'}
]

 

# 참고 : Pick  올바르게 사용하기

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

//to-be ~ 'K' 타입의 범위를 좁히기
type Pick<T, K extends keyof T> = {
  [k in K]: T[k]
}

// extend는 '확장'이 아닌 '부분집합' 개념이라는 걸 이해하자