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

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

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

DRY(Don’t repeat yourself) 원칙에 따라 코드를 개선해볼 수 있다.

함수, 상수, Loop 를 통해 개선했다.

/** 원본 코드 (개선 전) */
console.log('Cylinder 1 x 1 ',
            'Surface area: ', 6.283185 * 1 * 1 + 6.283185 * 1 * 1,
            'Volume: ', 3.14159 * 1 * 1 * 1
            )
console.log('Cylinder 1 x 2 ',
            'Surface area: ', 6.283185 * 1 * 1 + 6.283185 * 2 * 1,
            'Volume: ', 3.14159 * 1 * 2 * 1
            )
console.log('Cylinder 2 x 1 ',
            'Surface area: ', 6.283185 * 2 * 1 + 6.283185 * 2 * 1,
            'Volume: ', 3.14159 * 2 * 2 * 1
            )
/** 수정 코드 (개선 후) */
const surfaceArea = (r, h) => 2 * Math.PI * r * (r + h);
const volume = (r, h) => Math.PI * r * r * h;
for (const [r, h] of [[1, 1], [1, 2], [2, 1]]) {
    console.log(
        'Cylinder ${r} x ${h} ',
        'Surface area: ${surfaceArea(r, h)}',
        'Volume: ${volume(r, h)}'
    );
}



타입 중복은 어떻게 해결할 수 있을까? #

타입 중복은 코드 중복만큼이나 많은 문제를 발생시킨다.

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

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

여기에 middleName 과 같은 속성이 추가되면 별도의 클래스가 한 개 더 생성될 것이다.


타입에서도 DRY 원칙을 적용시킬 수 있다.

/* 예시 : 시그니처 추출/분리 */
/* 개선 전 */
function get(url: stirng, opts: Options): Promise<Response> { /* ... */}
function post(url: stirng, opts: Options): Promise<Response> { /* ... */}

/* 개선 후 */
type HttpFunction = (url: string, opts: Options) => Promise<Response>;
const get: HttpFunction = (url, opts) => { /* ... */}
const post: HttpFunction = (url, opts) => { /* ... */}
/* 예시 : 상속 */
/* 개선 전 */
interface Person {
    firstName: string;
    lastName: string;
}

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

/* 개선 후 */
interface Person {
    firstName: string;
    lastName: string;
}

interface PersonWithBirthDate extends Person {
    birth: Date;
}

// 아래와 같은 방법도 가능하다.
// type PersonwithBirthDate = Person & { birth: Date };
/* 개선 전 */
interface State {
    userId: string;
    pageTitle: string;
    recentFiles: string[];
    pageContents: string;
}
interface TopNavState {
    userId: string;
    pageTitle: string;
    recentFiles: string[];
}

/* 개선 후 */
interface State {
    userId: string;
    pageTitle: string;
    recentFiles: string[];
    pageContents: string;
};

type TopNavState {
    userId: State['userId'];
    pageTitle: State['pageTitle'];
    recentFiles: State['recentFiles'];
    // State 의 부분 집합으로 TopNavState 를 정의한다.
    // State 를 인덱싱하여 '속성의 타입'에서  중복을 제거할 수 있다.
};

type TopNavState {
    [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
    // '매핑된 타입'을 사용해서 조금 더 개선할 수 있다.
};

‘매핑된 타입’은 ‘배열의 필드’를 ‘루프(Loop)’ 도는 것으로 이해하면 된다.

type Pick<T, K> = { [k in K]: T[k] };

type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
interface SaveAction {
    type: 'save';
    //...
}
interface LoadAction {
    type: 'load';
    //...
}
type Action = SaveAction | LoadAction;

type ActionType = 'save' | 'load';
type ActionType = Action['type'] // 타입은 "save" | "load"

// [참고] type ActionRec = Pick<Action, 'type'>; // {type: "save" | "load"}
/* 개선 전 */
interface Options {
    width: number;
    height: number;
    color: string;
    label: string;
}
interface OptionsUpdate {
    width?: number;
    height?: number;
    color?: string;
    label?: string;
}

/* 개선 후 */
interface Options {
    width: number;
    height: number;
    color: string;
    label: string;
}
type OptionsUpdate = {[k in keyof Options] ?: Options[k]};
// type OptionsUpdate = {[k in "width" | "height" | "color" | "label"] ?: Options[k]};
// keyof Options => "width" | "height" | "color" | "label"

typeof : ‘값’으로부터 ‘타입’을 정의할 수도 있다.

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

type Options = typeof INIT_OPTIONS;
/* 아래와 같다. */
/*
interface Options {
    width: number;
    height: number;
    color: string;
    label: string;
}
*/

값으로부터 타입을 만들어 낼 때는 선언의 순서에 주의해야 한다.

되도록 타입 정의를 먼저 하고 값이 그 타입에 할당 가능하다고 선언하는 것을 권장한다.
그래야 타입이 더 명확해지고, 예상이 가능해진다.
즉, 값으로부터 추출하는 것 보다 타입을 먼저 정의하는 것을 권장한다. (?)



ReturnType : 함수/메서드의 반환 값에 명명된 타입을 추출할 수 있다.

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

type UserInfo = ReturnType<typeof getUserInfo>;
// type UserInfo = { userId: string; name: string; age: number; ...}

ReturnType 은 함수의 ‘값’인 getUserInfo 가 아니라 함수의 ‘타입’인 typeof getUserInfo 에 적용된 것에 주의하자.

이러한 표준 기술을 사용할 때 대상이 ‘값’인지, ‘타입’인지 확실하게 인지해야 한다.



요약 & 정리 #

  • 현재 ~ 앞으로 다양한 주제를 이야기하게 될 것이지만 원래의 목표(유효한 프로그램은 통과시키고 무효한 프로그램은 오류를 발생시키는 것)를 잊으면 안된다.
  • DRY 원칙을 ‘타입’에도 최대한 적용해야 한다.
    • 타입에서 반복을 피할 수 있다.
  • 타입스크립트가 제공하는 도구들을 익히자. (keyof, typeof, 인덱싱, 매핑된 타입 등)
  • 표준 라이브러리 도구들을 익히자. (Pick, Partial, ReturnType)