본문 바로가기
Language/Typescript

타입챌린지 : 1097-IsUnion (medium)

by hsloth 2023. 4. 21.

 

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

 

https://github.com/type-challenges/type-challenges/blob/main/questions/01097-medium-isunion/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

 

제네릭 인자 T를 받아서, 이 T가 유니온 타입인지 확인하는 타입이다.

진짜... 어려워 죽는줄 알았다. 결국에 다른 분들의 답을 봤지만, 다른 분들의 답이 왜 이렇게 되는건지 이해하기까지도 꽤 걸렸다.

일단, 참고할만한 포스팅은 Permutation이다.

https://suloth.tistory.com/67

 

타입챌린지 : 296-Permutation (medium)

이 글은 제가 타입챌린지를 하면서 해석한 내용을 적는 글입니다. 틀린 내용이 있으면 댓글 달아주시면 감사하겠습니다. https://github.com/type-challenges/type-challenges/blob/main/questions/00296-medium-permutation

suloth.tistory.com

 

// 1번째 답
type IsUnion<T, K = T> = [T] extends [never] ? false : 
	T extends K ? ([K] extends [T] ? false : true) : never;


// 2번째 답
type IsUnion<T, K = T> = (T extends T ? K extends T ? true : unknown : never) 
	extends true ? 
	false : true;
    

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

type cases = [
  Expect<Equal<IsUnion<string>, false>>,
  Expect<Equal<IsUnion<string | number>, true>>,
  Expect<Equal<IsUnion<'a' | 'b' | 'c' | 'd'>, true>>,
  Expect<Equal<IsUnion<undefined | null | void | ''>, true>>,
  Expect<Equal<IsUnion<{ a: string } | { a: number }>, true>>,
  Expect<Equal<IsUnion<{ a: string | number }>, false>>,
  Expect<Equal<IsUnion<[string | number]>, false>>,
  // Cases where T resolves to a non-union type.
  Expect<Equal<IsUnion<string | never>, false>>,
  Expect<Equal<IsUnion<string | unknown>, false>>,
  Expect<Equal<IsUnion<string | any>, false>>,
  Expect<Equal<IsUnion<string | 'a'>, false>>,
  Expect<Equal<IsUnion<never>, false>>,
]

 

 

내가 처음 쓴 한심한 답을 봐보자.

type IsUnion<T> = T extends infer F | infer O ? true : false;

이건 당연히 오답입니다. 대부분의 타입은 유니온 타입을 기본적으로 상속할 수 있기 때문에... 거의 모든 리턴 값이 true가 되어버린다.

 

그리고 어떻게 풀까... 고민을 하다가 Permutation에서 풀었던 분산 조건부 타입에서 Union Type과 Naked Type을 이용해서 풀어야되겠다 라는 생각은 들었다. 그런데 이걸 어떤식으로 코드를 짜던 도저히 답이 나오지 않았다 ㅋㅋㅋㅋ

그래서.. 결국 다른 분들의 답을 보았다.

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

처음에는 도저히 이해가 가지 않았다.

"왜 [K] extends [T]를 한거지? 어차피 분산조건부 타입에서 저렇게 쓰면 K == T 이기 때문에 의미가 없지 않나?" 라는 생각이 계~~~속 들었다.

 

그러다가... 문득 깨달았다. 예를 들어보면서 설명을 해보겠다!

type A<T> = T extends string ? 'yes' : 'no';

type B = A<1 | 'A'> // 'yes' | 'no';

/* -- process -- */
B = 1 extends string ? 'yes' : 'no' | 'A' extends string ? 'yes' : 'no'

분산 조건부 타입에서 Naked Type에 Union Type이 들어가게 되면, 해당 Union Type은 원소 하나씩 넣어서 연산하게 된다.

 

그런데, 답을 보면 T는 extends로 분산 조건부 타입을 사용하고 있지만, K는 extends를 사용하지 않아 분산 조건부 타입을 사용하지 않고 있다.

즉, K는 유니온 타입이 분산되지 않고, 그대로 유니온 타입 자체가 K에 대입되는 것이다.

다시 답을 봐보자.

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

[T] extends [never] ? false 는 never를 check 해주기 위해서 넣은 코드이다. Permutation을 읽고 분산 조건부 타입에서의 Naked  Type의 특징에 대해 알아보았다면, 이는 이해할 수 있을거라고 생각한다. 간단히 말하면 T extends never를 하면 뒤를 거치지 않고 바로 never를 리턴해서 배열을 씌워준 것이다.

T extends K ? : T에 분산 조건부 타입을 사용하기 위한 구문이다. T extends가 있어야 T에 원소 하나하나씩 대입이 된다. T extends any 같이 써도 상관없다. T를 분산 조건부 타입으로 사용할 수 있기만 하면 된다.

[K] extends [T] ? false : true : K는 extends를 해주지 않았다.(분산 조건부 타입에서 Naked Type의 성질에 따라서 배열을 씌워주면, 유니온 타입은 분산이 되지 않는다) 그래서 K는 분산 조건부 타입이 사용되지 않았기 때문에, K에 담긴 유니온타입이 분산되지 않고 그대로 사용된다. 즉, T가 1 | 2 라면 T는 분산 조건부 타입이 앞에서 사용되었기 때문에 아래와 같이 된다.

1 extends 1 | 2 ([1 | 2] extends [1] ? false : true) : never
|
2 extends 1 | 2 ([1 | 2] extends [2] ? false : true) : never

[1 | 2] extends [1] : 1 | 2 는 1을 상속할 수 있을까? 없다. 1 | 2가 1보다 범위가 넓기 때문에 1 | 2는 1을 가지지 못한다. 하지만, 그 반대인 1 extends 1 | 2는 1이 1 | 2를 가질 수 있기 때문에 상속받을 수 있다. 그래서, 이를 통해서 제네릭 T가 유니온인지, 아닌지를 판단할 수 있다. 만약 유니온이 아니라면 [1] extends [1] 이나 [string] extends [string], [[string | number]] extends [[string | number]] 와 같이 될 것이다.

따라서, [K] extends [T] ? false : true 는 T가 유니온 타입이라면 true를 리턴하고, 아니라면 false를 뱉는 구문이 된다.

 

 

그리고, 또 하나의 답

와... 감탄을 금치 못하겠다. 어떻게 이런 생각을 하는걸까? 되게 간단한 생각인데... 나는 왜 생각을 못할까?

다른 분들이 이해하기 쉬우라고 설명을 할 겸, 같이 적어 보겠다.

type IsUnion<T, K = T> = (T extends T ? K extends T ? true : unknown : never) 
	extends true ? 
	false : true;

T extends T ? K extends T ? : T extends T는 별로 의미없다. 그냥 T가 분산 조건부 타입을 사용하게 됨으로써 원소를 하나하나씩 집어넣기 위해 쓴 구문이다. T extends C, T extends any를 써도 된다.

그리고 K extends T를 해서 K또한 분산 조건부 타입을 사용해서 원소를 하나하나 집어넣게 한다.

여기서 중요 포인트가 있는데, T를 분산하고 K를 분산하는 것이기 때문에 모든 경우의 수가 다 나온다는 것이다.

 

예를들어 T가 1 | 2라고 해보자.

그러면 K = T이기 때문에, T extends T ? K extends T에서 만들어지는 경우의 수는 다음과 같다.

1 extends 1 ? 1 extends 1 => true

1 extends 1 ? 2 extends 1 => unknown

2 extends 2 ? 1 extends 2 => unknown

2 extends 2 ? 2 extends 2 => true

따라서 (K extends T ? K extends T ? true : unknown : never) = (true | unknown | unknown | true) = (true | unknown) 와 같다.

(true | unknown) extends true ? false : true 를 계산하면, (true | unknown)은 true를 상속받을 수 없기 때문에 true를 리턴값으로 뱉는다.

만약 T가 유니온이 아니었다면 (true | true | true | true) = true 가 되어서, true extends true ? false : true를 계산해서 결과가 false가 나왔을 것이다.