티스토리 뷰
아래 글에서 Split 타입 구현도 살펴볼 수 있습니다!
DeepMerge 타입을 구현합니다.
서론
이번 글에서는 두 인터페이스를 합성하여 하나의 인터페이스로 만들어 주는 DeepMerge 타입을 만들어 보려고 합니다.
음...단순히 인터섹션(&)을 이용하면 되는 것 아닌가요? 🤔
type DeepMerge<T extends object, P extends object> = T & P;
물론 인터섹션 타입을 이용하면 두 객체를 병합할 수 있습니다.
// 인터섹션 타입을 이용해 두 객체를 병합할 수 있지만
type DeepMerge<T extends object, P extends object> = T & P;
type Example = DeepMerge<{ a: { b: 1 } }, { a: { c: 2 } }>;
const e1: Example = {
a: {
b: 1,
c: 2,
},
};
하지만 두 키의 타입이 다른 경우, 인터섹션 타입은 우선순위를 정하지 못합니다.
// 같은 키에 대하여 우선 순위를 정하지 못합니다.
type DeepMerge<T extends object, P extends object> = T & P;
type Example2 = DeepMerge<{ a: 1 } , { a: 2 }>; // type Example = never
Example2의 경우 타입이 never로 추론되죠.
따라서 인터섹션 타입만으로는 우리가 기대하는 합성 객체 타입을 얻기 어렵습니다.
이를 개선해 우선순위를 정의하고, 우선순위에 따라 합성해 주는 DeepMerge<T, P>를 만들어 볼까요?
우선순위 정의
우선순위부터 정의하겠습니다. DeepMerge는 객체 타입 T, P를 타입 파라미터로 받습니다.
T와 P가 동일한 키가 존재하지만 대한 타입이 다르다면?
- 유니온 타입으로 변환할 것인가 (T와 P 타입 전부 허용)
- 우선순위를 통해 한 타입만을 반영할 것인가 ✅
저는 2번으로 구현해 보겠습니다.
T와 P 어느 쪽을 더 우선시할 것인가?
- T
- P ✅
후에 들어온 P가 우선시 되도록 타입을 구현하겠습니다.
이를 바탕으로 경우의 수를 작성해 보면 아래와 같습니다.
/**
*
* DeepMerge<T, P> 객체 T, P를 합성한다.
*
* 경우의 수
*
* 1. key가 T에 있고, P에는 없을 때 : T[key] 우선시한다.
*
* 2. key가 T에 있고, P에도 있는 경우
* - T[key]가 객체인 경우, P[key]도 객체인 경우 : T[key], P[key]의 객체를 병합한다.
* - T[key]가 객체인 경우, P[key]가 객체가 아닌 경우 : T[key]를 우선시한다.
* - T[key]가 객체가 아닌 경우, P[key]가 객체인 경우 : P[key]를 우선시한다.
* - T[key]가 객체가 아닌 경우, P[key]가 객체가 아닌 경우 : P[key]를 우선시한다.
*
*
* 3. key가 T없고, P에는 있을 때 : 무조건 P[key] 우선시한다.
*
*/
type DeepMerge<T, P> = any;
Merge 구현하기
합성 기능부터 구현해 볼까요?
주어진 객체의 키 값으로 새로운 타입을 만들기 위해 Mapped Type를 이용해 보겠습니다.
type DeepMerge<T extends object, P extends object> = {
[key in keyof T | keyof P]: any;
};
type Example2 = DeepMerge<{ a: 1, b: 1 } , { a: 2 }>; // type Example2 = { a: any; b: any; }
두 키를 유니온 타입으로 합성하여 T와 P의 모든 키를 꺼내고 그 키를 any로 매핑합니다.
Example2를 보면 모든 키가 any 타입으로 추론되는 것을 볼 수 있습니다.
type DeepMerge<T extends object, P extends object> = {
[key in keyof T | keyof P]: key extends keyof P
? P[key]
: key extends keyof T
? T[key]
: never; // key가 T, P에 포함되지 않는다. (키는 무조건 T와 P에 종속되기에 발생할 수 없는 경우의 수이다.)
};
type Example2 = DeepMerge<{ a: 1, b: 1 } , { a: 2 }>; // type Example2 = { a: 2; b: 1; }
Value Type을 추가하였습니다. 각 key에 맞게 값 타입을 지정해 준 건데요.
key가 P에서 온 거라면 `P[key] 값 타입`이고, T에서 온 거면 `T[key] 값 타입`이라는 의미입니다.
Example2를 보면 `a`에 P의 값 타입 `2`가 추론되는 것을 확인할 수 있습니다.
이제 다른 경우의 수도 반영해 보겠습니다.
type DeepMerge<T extends object, P extends object> = {
[key in keyof T | keyof P]: key extends keyof P
? key extends keyof T
? T에도 있고 P에도 있는 경우
: P[key] // key가 T에는 없고 P에는 있는 경우
: key extends keyof T
? T[key] // key가 T에는 있고 P에는 없는 경우
: never;
};
type Example2 = DeepMerge<{ a: 1, b: 1 } , { a: 2 }>; // type Example2 = { a: 2; b: 1; }
조건부 타입을 조금 수정해서 각 경우의 수를 나눴습니다.
큰 분기 3개에 대해서 작성한 겁니다. 3개 밖은 존재할 수 없으니 여전히 never 타입이 존재합니다.
/**
* 2. key가 T에, 있고 P에도 있는 경우
* - T[key]가 객체인 경우, P[key]도 객체인 경우 : T[key], P[key]의 객체를 병합한다.
* - T[key]가 객체인 경우, P[key]가 객체가 아닌 경우 : T[key]를 우선시한다.
* - T[key]가 객체가 아닌 경우, P[key]가 객체인 경우 : P[key]를 우선시한다.
* - T[key]가 객체가 아닌 경우, P[key]가 객체가 아닌 경우 : P[key]를 우선시한다.
*/
이어서 `key가 T에도 있고 P에도 있는 경우`를 작성해 보겠습니다.
경우의 수를 글로 써넣으면 아래와 같습니다.
type DeepMerge<T extends object, P extends object> = {
[key in keyof T | keyof P]: key extends keyof P
? key extends keyof T
? T[key]가 객체이고
? P[key]가 객체라면
? 맞다면, T[key], P[key] 객체를 병합한다.
: 아니라면, (T[key]만 객체라면) T[key]를 우선시 한다.
: 아니라면, (T[Key]가 객체가 아니라면) P[key]를 우선시 한다.
: P[key]
: key extends keyof T
? T[key]
: never;
};
코드로 바꿔볼까요.
type DeepMerge<T extends object, P extends object> = {
[key in keyof T | keyof P]: key extends keyof P
? key extends keyof T
? T[key] extends object
? P[key] extends object
? DeepMerge<T[key], P[key]>
: T[key]
: P[key]
: P[key]
: key extends keyof T
? T[key]
: never;
};
T[key]와 P[key]가 객체인지 조건부 타입을 추가하고, 정의한 우선순위에 맞춰 추론 타입들을 지정해 주면 구현 완료입니다!
아래 playground에서 전체코드를 확인해 볼 수 있습니다. ✌️
타입이 익숙하지 않으시다면, 조건부 타입을 써서 재귀문이나 순회를 이용해 타입을 구현하는 것이 어렵게 느껴지실 수 있습니다.
저는 알고리즘처럼 접근하는 방법을 추천드리고 싶은데요.
제가 경우의 수를 먼저 작성했듯이, 논리적인 흐름을 떠올려본 다음에 구현을 해보는 거죠.
감사합니다.
공부한 내용을 복습/기록하기 위해 작성한 글이므로 내용에 오류가 있을 수 있습니다.
'TS | NestJS' 카테고리의 다른 글
[NestJS] Exception filters 추가하기 (feat.Custom Exception) (0) | 2024.03.20 |
---|---|
[NestJS] Swagger 적용하기 (feat. API 문서화) (0) | 2024.03.15 |
[TS] 타입이 추론되는 String.prototype.split - 2 (0) | 2024.03.05 |
[TS] 타입이 추론되는 String.prototype.split - 1 (0) | 2024.03.03 |
[TS] 유틸리티(Utility) 타입 (0) | 2024.03.02 |