티스토리 뷰

TS | NestJS

[TS] DeepMerge 타입 구현해보기

rimo (리모) 2024. 3. 6. 10:48

아래 글에서 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가 동일한 키가 존재하지만 대한 타입이 다르다면? 

  1. 유니온 타입으로 변환할 것인가 (T와 P 타입 전부 허용)
  2. 우선순위를 통해 한 타입만을 반영할 것인가 ✅

저는 2번으로 구현해 보겠습니다.

 

 

T와 P 어느 쪽을 더 우선시할 것인가?

  1. T
  2. 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 Playground - An online editor for exploring TypeScript and JavaScript

The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.

www.typescriptlang.org

 

 

 

 

타입이 익숙하지 않으시다면, 조건부 타입을 써서 재귀문이나 순회를 이용해 타입을 구현하는 것이 어렵게 느껴지실 수 있습니다.

 

저는 알고리즘처럼 접근하는 방법을 추천드리고 싶은데요.

제가 경우의 수를 먼저 작성했듯이, 논리적인 흐름을 떠올려본 다음에 구현을 해보는 거죠.

 

 

 

감사합니다.

 


 

공부한 내용을 복습/기록하기 위해 작성한 글이므로 내용에 오류가 있을 수 있습니다.

 

댓글
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Total
Today
Yesterday