이펙티브 타입스크립트 (댄 밴더캄 지음) 를 읽고 정리
📍요약
✅같은 코드를 반복하지 말라 (DRY : Don't repeat yourself) 원칙을 타입에도 적용해야 한다
✅타입 인덱싱, extends를 통해 인터페이스 필드의 반복 줄일 수 있음
✅추가로 keyof, typeof, 매핑된 타입을 활용
✅제네릭 타입은 타입을 위한 함수이며 제네릭 매개변수를 제한하기 위해 extends 활용 가능
✅Pick, Partial, ReturnType 같은 유틸리티 타입에 익숙해야 함
📍코드 반복 줄이기 1
사용한 개념
1️⃣타입 인덱싱
2️⃣매핑된 타입
3️⃣Pick 유틸리티 타입
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
interface TopNavState {
userId: string;
pageTitle: string;
recentFiles: string[];
}
// TopNavState 를 확장하여 State를 구성하는 것보다
// State의 부분 집합으로 TopNavState를 정의하는 것이 바람직하다
// 1. State 타입의 프로퍼티를 인덱싱하여 참조할 수 있다
type TopNavState1 = {
userId: State["userId"];
pageTitle: State["pageTitle"];
recentFiles: State["recentFiles"];
}
// 2. 매핑된 타입을 사용하면 중복을 더 줄일 수 있다
type TopNavState2 = {
[k in "userId" | "pageTitle" | "recentFiles"]: State[k]
}
// 3. 매핑된 타입은 Pick 유틸리티 타입과 동일하다
type TopNavState3 = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
// Pick<T, K extends keyof T> = { [P in K]: T[P]; }
- enum은 tree-shaking이 안되서 union을 더 많이 사용한다
📍유니온 타입 vs Pick
interface SaveAction {
type: 'save';
// ...
}
interface LoadAction {
type: 'load';
// ...
}
type Action = SaveAction | LoadAction;
type ActionType = "save" | "load"; // 타입 반복
// 타입 인덱싱하여 반복 줄이기
type ActionType1 = Action['type']; // Type is "save" | "load"
// Pick을 사용하면 type 프로퍼티를 갖는 interface가 만들어진다
type ActionRec = Pick<Action, "type">; // {type: "save" | "load"}
📍코드 반복 줄이기 2
사용한 개념
1️⃣매핑된 타입
2️⃣keyof 연산자
3️⃣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 OptionsUpdate2 = { [k in keyof Options]?: Options[k] }
// keyof는 타입을 받아서 속성 타입의 유니온을 반환
type OptionsKeys = keyof Options; // Type is "width" | "height" | "color" | "label"
- 매핑된 타입, keyof, ? 패턴은 Partial 유틸리티 타입과 동일하다
위에서 만들어본 OptionsUpdate 를 Partial로 대체할 수 있다
interface Options {
width: number;
height: number;
color: string;
label: string;
}
class UIWidget {
constructor(init: Options) { /* ... */ }
update(options: Partial<Options>) { /* ... */ }
}
📍코드 반복 줄이기 3
사용한 개념
1️⃣typeof 연산자
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
interface Options {
width: number;
height: number;
color: string;
label: string;
}
// typeof 연산자를 통해 INIT_OPTIONS 객체의 필드 값의 타입을 추출
type Options2 = typeof INIT_OPTIONS;
📍ReturnType 유틸리티 타입
- ReturnType 유틸리티 타입을 통해 (함수의) 리턴 타입을 추출할 수 있다
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
function getUserInfo(userId: string) {
// COMPRESS
const name = 'Bob';
const age = 12;
const height = 48;
const weight = 70;
const favoriteColor = 'blue';
// END
return {
userId,
name,
age,
height,
weight,
favoriteColor,
};
}
// Return type inferred as { userId: string; name: string; age: number, ... }
// ReturnType 유틸리티 타입을 통해 리턴 타입을 추출할 수 있다
type UserInfo = ReturnType<typeof getUserInfo>
📍제너릭 매개변수
아래 코드에서는 제너릭 매개변수 대신 객체 리터럴 타입을 사용했다
{first: string} 이 Name을 extends 하지 않아서 에러가 발생한다
interface Name {
first: string;
last: string;
}
type DancingDuo<T extends Name> = [T, T]; // Name이 T의 부분집합
const couple1: DancingDuo<Name> = [
{first: 'Fred', last: 'Astaire'},
{first: 'Ginger', last: 'Rogers'}
]; // OK
const couple2: DancingDuo<{first: string}> = [
// last 프로퍼티가 누락되어 에러 발생
{first: 'Sonny'},
{first: 'Cher'}
];
type FirstLast = Pick<Name, 'first' | 'last'>; // OK
type FirstMiddle = Pick<Name, 'first' | 'middle'>; // Name에 없는 키는 쓸 수 없음
Pick의 정의 살펴보기
type Pick<T, K> = {
[k in K]: T[k] // 에러 : K 타입은 string | number | symbol 타입에 할당할 수 없다
// 즉, K 가 객체의 프로퍼티가 될 수 있는지 모르기 때문에 에러 발생
}
// K는 T의 부분집합이 되면 에러 해결
type Pick<T, K extends keyof T> = {
[k in K]: T[k] // 정상
}