챌린지

입력 타입 T를 받아서 T가 유니온 타입인지를 반환하는 제네릭 타입 IsUnion을 구현해보세요.

예시:

type case1 = IsUnion<string>; // false
type case2 = IsUnion<string | number>; // true
type case3 = IsUnion<[string | number]>; // false

해답

이런 종류의 챌린지를 만날 때면 쉽게 해답을 떠올리기 어렵습니다. 해당 타입을 구현 할 때 사용할 수 있는 일반적인 풀이가 없기 때문입니다. 사용할 수 있는 내장 타입이 나 빌트인 타입도 존재하지 않습니다.

따라서 주어진 것을 활용할 수 있도록 창의적으로 접근해야 합니다. 유니온과 유니온 이 무엇을 표현하는지 생각해보는 것부터 시작하겠습니다.

string과 같은 기본 타입은 string 외의 다른 타입이 될 수 없습니다. string | number와 같은 유니온 타입을 사용하면 string 또는 number 중 하나의 값을 얻는 걸 기대할 수 있습니다.

일반 타입은 가능한 값들의 집합을 표현할 수 없지만 유니온은 할 수 있습니다. 일반 타입이 분산된 형태는 가능하지 않지만 유니온에서는 가능합니다.

위의 중요한 차이점을 통해 주어진 타입이 유니온인지 확인할 수 있습니다. 유니온이 아닌 경우에 타입 T가 분산될 때에는 아무 변화도 일어나지 않습니다. 유니온의 경 우엔 많은 것이 변할 수 있습니다.

타입스크립트에는 분산 조건부 타입이라는 멋진 타입이 있습니다. T가 유니온인 경 우에 T extends string ? true : false와 같은 구조를 사용하면 조건이 분산되어 적 용됩니다. 이는 유니온의 각 원소에 조건부 타입이 적용되는 것처럼 볼 수 있습니다.

type IsString<T> = T extends string ? true : false;

// For example, we provide type T = string | number
// It is the same as this
type IsStringDistributive = string extends string
  ? true
  : false | number extends string
  ? true
  : false;

이제 풀이가 어떤 방향으로 나아가고 있는지 보이시나요? 타입 T가 유니온일 경우, 분산 조건부 타입을 사용하여 유니온을 쪼갠 뒤 원래의 입력 타입 T와 비교할 수 있 습니다. 이 경우, 비교 결과가 같다면 유니온이 아닌 것입니다. 유니온일 경우 그 결 과는 같지 않을 것입니다. string | numberstring에 할당할 수 없고 number에도 할당할 수 없습니다.

이제 구체적인 구현을 해보겠습니다! 먼저 입력 타입 T의 복사본을 만들어 타입 T가 수정되지 않은 형태를 보존해둡니다. 이 타입을 나중에 비교하는 데에 사용할것 입니다.

type IsUnion<T, C = T> = never;

조건부 타입을 적용하여 분산을 표현할 수 있습니다. 조건부 타입의 “true” 분기 내에 서 유니온의 각 원소를 얻을 수 있습니다.

type IsUnion<T, C = T> = T extends C ? never : never;

이제 가장 중요한 부분입니다. 각 원소를 원래 입력 타입인 T와 비교합니다. 이 경 우 두 타입이 동일하다면 분산이 이루어지지 않은 것이므로 유니온이 아닙니다. false를 반환해줍니다. 반대의 경우 분산이 적용된 것이고 유니온의 각 원소는 유니 온과 비교됩니다. 그 경우 유니온인 것이므로 true를 반환합니다.

type IsUnion<T, C = T> = T extends C ? ([C] extends [T] ? false : true) : never;

끝입니다! 명확히 하기 위해 조건부 분산 타입의 “true” 분기에서 [C][T]가 어 떻게 동작하는지 예시를 남기겠습니다.

string과 같은 유니온이 아닌 타입에서 두 타입은 같습니다. 그 결과로 false를반 환합니다.

[T] = [string][C] = [string];

string | number와 같은 유니온을 전달할 경우 두 타입은 달라집니다. 복사본인 C는 튜플 내부에 유니온을 가지고 있고 T는 튜플 내부에 일반 타입을 가지고 있습 니다. 따라서 유니온임을 알 수 있습니다.

[T] = [string] | [number]
[C] = [string | number]

참고