Programming/TypeScript
[TypeScript] 타입 연산과 제너릭 사용으로 반복 줄이기
앵도라지
2023. 4. 22. 23:16
이 글은 이펙티브 타입스크립트 '아이템 14 타입 연산과 제너릭 사용으로 반복 줄이기'를 정리한 글입니다.

- 코드를 짤 때는 같음 코드를 반복하지 말라는 DRY (don’t repeat yourself) 원칙이 있다.
- 타입에서도 같은 원칙을 적용해야 한다.
- 타입 중복은 많은 문제를 발생시킨다.
방법 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 distance(a: Point2D, b: Point2D) {
/* ... */
}
방법 2 : 같은 시그니처를 공유하고 있는 함수는 명명된 타입으로 분리해 반복 줄이기
//같은 시그니처 공유
function get(url: string, opts: Options): Promise<Response> {
/* COMPRESS */ return Promise.resolve(new Response()) /* END */
}
function post(url: string, opts: Options): Promise<Response> {
/* COMPRESS */ return Promise.resolve(new Response()) /* END */
}
//명명 타입으로 분리
type HTTPFunction = (url: string, options: Options) => Promise<Response>
const get: HTTPFunction = (url, options) => {
/* COMPRESS */ return Promise.resolve(new Response()) /* END */
}
const post: HTTPFunction = (url, options) => {
/* COMPRESS */ return Promise.resolve(new Response()) /* END */
}
방법3 : 한 인터페이스가 다른 인테페이스를 확장
interface Person {
firstName: string
lastName: string
}
interface PersonWithBirthDate extends Person {
birth: Date
}
방법 4 : 이미 존재하는 타입을 확장하는 경우에는 인터섹션 연산자(&) 사용
interface Person {
firstName: string
lastName: string
}
type PersonWithBirthDate = Person & { birth: Date }
방법 5 : State 인덱싱 → 중복 제거
//방법 5, 6, 공통 예제
//전체 상태를 표현하는 State 타입과 부분만 표현하는 TopNavState가 있는 경우
interface State {
userId: string
pageTitle: string
recentFiles: string[]
pageContents: string
}
interface TopNavState {
userId: string
pageTitle: string
recentFiles: string[]
}
type TopNavState = {
userId: State['userId']
pageTitle: State['pageTitle']
recentFiles: State['recentFiles']
}
방법 6 : 매핑된 타입을 사용
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
}
- 매핑된 타입
- 매핑된 타입은 in 키워드를 사용해 배열이나 튜플 등의 타입에 대해 루프를 도는 것과 같은 방식으로 새로운 타입을 만들어내는 기능이고, 주로 객체나 배열의 타입변환에 사용된다.
- 이 패턴은 표준 라이브러리에서도 일반적으로 찾을 수 있으며, Pick 이라고 한다.
- Pick
- Pick 은 제네릭 타입으로 첫번째 인자로 객체타입을, 두번째 인자로 해당 객체 타입에서 추출하고자 하는 필드의 이름들을 문자열 리터럴 타입 배열로 받는다.
- Pick 을 사용해서 State 에서 userId, pageTtiel, recentFiles 필드만 추출해서 간단하게 새로운 타입을 만들 수 있다.
type Pick<T, K> = { [k in K]: T[k] };
방법 7 : 유니온 인덱싱
- 중복 발생 : 태그된 유니온에서도 다른 형태의 중복이 발생할 수 있다.
- 태그된 유니온 : 유니온 타입에 문자열 리터럴 타입을 추가해서 타입 안전성을 높인 방법
interface SaveAction {
type: 'save'
// ...
}
interface LoadAction {
type: 'load'
// ...
}
type Action = SaveAction | LoadAction
type ActionType = 'save' | 'load' // Repeated types!
//Action 유니온 인덱싱을 하면 타입 반복없이 ActionType 을 정의할 수 있다
type ActionType = Action['type'] // Type is "save" | "load"
type ActionRec = Pick<Action, 'type'>; //{type: "save" | "load"}
방법 8 : keyof
- keyof 는 타입을 받아서 속성 타입의 유니온을 반환한다
interface Options{
width: number;
height: number;
ocolor: string;
label: string;
}
interface OptionsUpdate{
width?: number;
height?: number;
color?: string;
label?: string;
}
type OptionsUpdate = {[k in keyof Options]?: Options[k]};
방법 9 : typeof
- 값의 형태에 해당하는 타입을 정의하고 싶을 때 사용
//사용 전
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
interface Options{
width: number,
height: number,
color: string,
label: string,
}
//사용 후
type Options = typeof INIT_OPTIONS
방법 10 : ReturnType
- 함수나 메서드의 반환값에 명명된 타입을 만들고 싶을 수 있다.
- ReturnType 을 사용해 함수의 반환 타입을 변수로 선언하거나 다른 타입의 매개변수로 전달할 수 있다.
function getUserInfo(userId: string){
//...
return {
userId,
name,
age,
height,
weight,
favoriteColor
};
}
type UserInfo = ReturnType<typeof getUserInfo>;
- 위 코드에서 ReturnType 은 함수의 타입인 typeof getUserInfo 에 적용되었다.
- 적용대상이 값인지 타입인지 정확하게 알고 구분해서 처리해야 한다.
방법 11 : 제너릭타입
- 제너릭 타입은 타입을 위한 함수와 같습니다.
- 제네릭 타입은 타입스크립트에서 DRY 원칙을 적용하는 핵심 방법 중 하나다.
- 제너릭을 사용하면 타입의 중복을 최소화하고 재사용성을 높일 수 있다.
function addNumbers(a: number, b: number): number {
return a + b;
}
function concatenateStrings(a: string, b: string): string {
return a + b;
}
//제너릭으로 변경
function combine<T>(a: T, b: T): T {
return a + b;
}
- combine 함수는 T 라는 타입의 매개변수를 사용한다.
- T 는 함수를 호출할 때 전달된 값의 타입으로 결정됩니다. 따라서 이 함수는 어떤 타입의 값을 합쳐서 반환할 수 있다.
- 제너릭 타입에서는 매개변수를 제한할 수 있는 방법이 필요다. (타입안전성을 보장하기 위해서)
- 제너릭 타입을 사용하면 여러 종류의 값들을 다룰 수 있는데, 제너릭 타입이 특정한 종류의 값만 다루도록 제한해야 하는 경우가 있다.
- extends 를 이용하면 제너릭 매개변수가 특정 타입을 확장한다고 선언할 수 있고 이를 통해 매개변수를 제한할 수 있다.
//제네릭 타입 매개변수 T 는 Name을 확장한다.
//DancingDuo 타입은 T 타입의 배열이고, 요소는 Name 타입 객체로 이뤄져있다.
interface Name{
first: string;
last: string;
}
type DancingDuo<T extends Name> = [T,T];
방법 12 : Pick
type Pick<T, K extends keyof T> = {
[k in K]: T[k];
};
- Pick 타입은 T 타입에서 K 타입에 포함된 속성만 선택해 새로운 타입을 정의한다.
- 이 때 in 키워드를 사용한 매핑타입을 이용해서 새로운 객체 타입을 생성한다.
요약
- DRY 원칙을 타입에도 최대한 적용해야 한다.
- 타입에 이름을 붙여서 반복을 피하고, extends 를 사용해서 인터페이스 필드의 반복을 피해야한다.
- 타입들간의 매핑을 위해 매핑된 타입 keyof,typeof,인덱싱,매핑된 타입들을 공부해야한다.
- 제너릭 타입은 타입을 위한 함수와 같다. 타입을 반복하는 대신 제너릭 타입을 사용하여 타입들 간에 매핑을 하는 것이 좋다.