본문 바로가기
Language/Typescript

타입챌린지 : 599-Merge (medium)

by hsloth 2023. 4. 15.

 

이 글은 제가 타입챌린지를 하면서 해석한 내용을 적는 글입니다. 틀린 내용이 있으면 댓글 달아주시면 감사하겠습니다.

 

https://github.com/type-challenges/type-challenges/blob/main/questions/00599-medium-merge/README.md

 

GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

Collection of TypeScript type challenges with online judge - GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

github.com

 

두 개의 타입을 제네릭으로 받아서 속성을 합치는 타입이다. 이 때, 속성이 겹친다면 두 번째 타입의 속성의 타입을 따른다.

 

이 문제를 풀다가, 같은 이름의 다른 타입인 속성을 가진 두 타입을 intersection Type으로 합치면, 해당 속성의 타입은 never가 된다는 걸 알아냈다.

예를들면,

type A = {
	a: number
    b: string
}

type B = {
	b: number
    c: boolean
}

type C = A & B;

// 아래껀 그냥 타입을 나타낸 것이니, 제대로된 코드가 아니다. 참고만 하자.
const c:C = {
	a: number
   	b: never
   	c: boolean
}

참고로 never 타입에는 값을 대입할 수가 없다. null조차 들어가지 않는다.

 

그래서 이를 이용해서 타입을 작성해보았다.

type Merge<F, S> = {
  [P in keyof (F & S)]: P extends keyof S ? S[P] : 
  P extends keyof F ? F[P] : never;
}


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type Foo = {
  a: number
  b: string
}
type Bar = {
  b: number
  c: boolean
}

type cases = [
  Expect<Equal<Merge<Foo, Bar>, {
    a: number
    b: number
    c: boolean
  }>>,
]

[P in keyof (F & S)] : F & S를 한 타입의 key를 P라고 정의한다. 즉 위의 예로 Foo & Bar 라고 하면, a, b, c를 가리킨다. 이때, a,b,c의 타입은 중요하지 않다(뒤에서 타입을 정해줄 것이기 때문에).

P extends keyof S ? S[P] : P가 S의 key를 상속한다면 S[P]를 타입으로 한다. 두 번째 인자인 S가 속성의 타입을 정하는데에 있어서 우선순위를 가지기 때문에 먼저 따져주었다.

P extends keyof F ? F[P] : never; : P가 F의 key를 상속한다면 F[P]를 타입으로한다. 그것도 아니면 never를 리턴한다.

 

아니면, 다음과 같은 답을 쓸 수도 있다.

type Merge<F, S> = {
  [P in (keyof F) | (keyof S)]: P extends keyof S ? S[P] : 
  P extends keyof F ? F[P] : never;
}

내가 처음 작성한 답에서 약간 변형을 시킨 답이다.

[P in (keyof F) | (keyof S)] : F의 key와 S의 key를 Union타입으로 묶어서 그 원소들을 P라고 정의한다.

그 뒷 부분은 위에 설명과 동일하다.

 

여기서 궁금증이 생기실 분들도 있을 수 있다.

위의 답에서는 Intersection Type(&)을 사용했는데, 왜 아래 답에서는 Union Type( | )을 사용한 것일까?

 

간단하게 생각하자. Foo와 Bar를 Union Type으로 묶으면, b를 제외한 나머지 속성들은 없어도 되는(optional) 속성이기 때문에 해당 타입은 b라는 속성만을 가지는 타입이 되버리게 된다.

그래서 다른 속성들도 강제로 가지게 하는 타입을 만들려면 Intersection Type을 사용해야한다.

 

그리고, keyof F | keyof S 는 문자열이기 때문에 Intersection Type(&)를 사용하면 두 조건을 모두 충족하는 타입을 만들어 버리기 때문에 두 문자열의 교집합을 타입으로 갖게되고, Union Type( | )를 사용하면 두 조건 중 하나만 충족해도 되기 때문에 모든 문자열을 포함하는 유니온 타입이 된다.

 

즉, 타입 레벨에서의 &와 | 에 대한 해석과, 문자열 레벨에서의 &와 | 에 대한 해석이 다르기 때문이라고 생각하면 좋을 것 같다.

뜻은 &는 모두 만족하는 이라는 뜻이고, | 는 둘 중 하나라도 만족하는 이라는 뜻인데,

타입 레벨에서 &는 모두 만족해야하기 때문에 모든 속성을 가지고 있어야하고, 문자열 레벨에서 &는 모두 만족해야하기 때문에 공통된 문자열만 리턴한다.

타입 레벨에서 | 는 하나라도 만족해야 하기 때문에 두 타입의 공통 속성만을 가지고 있으면 되고, 문자열 레벨에서 | 는 하나라도 만족해야 하기 때문에 일단 모든 문자열을 리턴할 수 있어야 한다.