아이템 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
)