티스토리 뷰

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

 

 

 

 

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

 

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

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

 

 

 

감사합니다.

 


 

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

 

댓글
«   2024/09   »
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