개발자라면 깔끔하고 간결한 코드를 적기 위하여 DRY(don't repeat yourself)
원칙을 지켜야 한다. 이 부분은 타입에 대해서도 적용되며, 반복되지 않고 깔끔한 코드를 작성하기 위하여 노력해야 한다.
타입에 대한 반복을 줄일 수 있는 방법을 하나씩 알아보도록 하자.
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) {/*...*/}
이에 대한 내용은 타입에 대한 것 뿐만 아니라 함수 시그니처에도 해당되는 말이다.
function get(url: string, opts: Options): Promise<Response>
function post(url: string, opts: Options): Promise<Response>
해당 코드를 더욱 간단하게 작성할 수 있다.
type HTTPFunction = (url: string, opts: Options) => Promise<Response>
const get: HTTPFunction = (url, opts) => {/*...*/}
그리고 중복되는 속성은 아래와 같이 정리할 수 있다.
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate extends Person{
birth: Date;
}
이렇게 작성하면 Person에 대한 특성을 이어받아 날짜에 대한 프로퍼티를 추가할 수 있게 된다. 부분집합을 공유하고 있다면, 공통 필드만 골라서 기반 클래스로 분리해 내는 것이다.
또한 이미 존재하는 타입일 경우에는 인터섹션 연산자를 사용할 수도 있습니다.
type PersonWithBirthDate = Person & { birth: Date}
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
이러한 인터페이스가 있다고 하고, 다음 인터페이스를 구성하고 싶다고 하자.
interface TopNavState {
userId: string;
pageTitle: string;
recentFiles: string[];
}
State의 부분 집합으로 TopNavState를 정의해볼 수 있겠다. 이 때, State를 인덱싱하여 타입에서 중복을 제거해보자
type TopNavState = {
userId: State['userId'];
pageTitle: State['pageTitle'];
recentFiles: State['recentFiles'];
};
이렇게 작성해도 아직 중복은 제거된 것이 아니다.
아래와 같이 매핑된 타입을 이용해 보자.
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
매핑된 것은 배열의 필드를 루프 도는 것과 같은 방식이며, 이는 표준 라이브러리를 통하여 코드를 개선할 수 있다.
이것은 Pick이라고 한다.
type Pick<T,K> = {[k in K]: T[k]};
이는 정의가 완전한 것은 아니다. (오류가 발생 가능함) 그래서 이를 조금 더 명확히 작성하면 아래와 같이 작성할 수 있겠다.
type TopNavstate = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
여기서 Pick은 제네릭 타입이다. 함수의 관점으로 본다면 더욱 편리하게 이해할 수 있는데, 함수처럼 두 개의 매개변수 값을 받아서 결과값을 반환하는 것이라고 이해하자.
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"}
확실히 위에 정의한 ActionType
과 다른 형식을 가지고 있다. 다른 라이브러리를 사용하였을 때 얻을 수 있는 타입의 형태도 잘 고려해야 한다.
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 OptionsUpdate = {[k in keyof Options]?: Options[k]};
keyof
는 타입을 받아, 속성 타입의 유니온을 반환한다.
type OptionKeys = keyof Options;
//타입이 "width"| "height" | "color" | "label"
이러한 문제도 라이브러리를 이용하여 간단하게 해결해보고자 할 수 있다.
class UIWidget {
constructor(init: Options) {/*...*/}
update(options: Partial<Options>) {/*..*/}
}
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
interface Options {
width: number;
height: number;
color: string;
label: string;
}
이런 경우 간단하게 typeof
를 사용하면 된다.
type Options = typeof INIT_OPTIONS;
function getUserInfo(userId: string){
//...
return {
userId,
name,
age,
height,
weight,
favoriteColor,
};
}
//추론된 반환 타입은 { userId: string; name: string; age: number,...}
이런 경우 ReturnType
제너릭을 사용하면 된다.
type UserInfo = ReturnType<typeof getUserInfo>;
함수의 값인 getUserInfo
가 아니라, 함수의 타입인 typeof getUserInfo
에 ReturnType
이 적용되었다. 적용 대상이 값인지, 타입인지 정확하게 알고 사용해야 한다. 주의할 것!
제너릭 타입은 타입을 위한 함수이다. 이 때 주의해야 할 점이 있다.
우리가 함수에서 매개변수로 매핑할 수 있는 값을 제한하기 위하여 타입 시스템을 사용하는 것처럼, 제너릭 타입에서 매개변수를 제한할 수 있는 방법이 필요하다. 이 때, 이를 제한할 수 있는 방법은 extends
를 이용하는 것이다.
그래서, 앞에 나온 Pick의 정의도 불완전하다고 이야기하였는데 이를 extends를 이용하여 완성할 수 있다.
type Pick<T, K> = {
[k in K]: T[k]
// ~'K' 타입은 'string | number | symbol' 타입에 할당 가능
위에서 에러 메세지를 띄워 준 것처럼, 제네릭은 어떤 값이라도 들어올 수 있으므로 K
값이 들어올 수 있는 범위가 너무 넓다. 그리고 T타입과 무관하다. K는 실제로 T의 부분 집합, 즉 keyof T가 되어야 한다.
type Pick<T, K extends keyof T> = {
[k in K]: T[k]
};
여기서 중요한 포인트는 extend
를 확장이 아닌, 부분집합의 개념으로 생각한다면 더욱 이해가 잘 될 것이다 :)