프로필
프론트엔드 개발자
김동희입니다

타입스크립트에서 두 타입이 서로 일치하는지 확인하는 방법

타입스크립트에서 두 타입이 일치한다는 건 어떻게 알 수 있을까요? 타입이란 할당 가능한 값의 집합이기 때문에 두 타입이 일치한다는 것은 각 타입에 할당 가능한 값의 집합이 서로 일치한다는 것과 동일한 의미입니다. 아래에서는 두 집합이 일치하는지 확인하는 방법을 통해 두 타입이 일치하는 지 확인해보도록 하겠습니다.

명제1

두 집합 AABB가 있을 때, AABB의 부분집합이고 BBAA의 부분집합이면, AABB는 동일한 집합이다

(증명)

AABB의 부분집합이고 BBAA의 부분집합일 때, AABB가 서로 다른 집합이라고 가정해보겠습니다.

그렇다면 AA에는 속하지만 BB에는 속하지 않는 원소 aa가 존재합니다.

그러나 AABB의 부분집합이므로 AA에 속하는 임의의 원소는 모두 BB에 속하게 되므로, 원소 aa는 존재할 수 없습니다.

따라서 가정이 잘못되었으므로, AABB의 부분집합이고 BBAA의 부분집합이면, AABB는 동일한 집합임을 알 수 있습니다.


타입스크립트에서 extends 키워드는 ‘~에 할당 가능한'과 비슷하게, ‘~의 부분 집합’이라는 의미로 받아들일 수 있습니다. ‘서브타입’이라는 용어는 어떤 집합이 다른 집합의 부분 집합이라는 의미입니다. extends 키워드는 제네릭 타입에서 한정자로도 쓰이며, ~의 부분 집합을 의미합니다.

명제1을 타입스크립트로 그대로 옮기면 아래와 같습니다.

type Equal1<A, B> = A extends B ? (B extends A ? true : false) : false;

그러나 안타깝게도 위의 Equal1은 기대한대로 작동하지 않는데요, 그 이유는 제네릭 타입이 유니언 타입일 때는 extends 키워드에서 분배법칙이 성립하기 때문입니다.

declare const test1: Equal1<1 | 2, 1>; // test1 expected: false, actual: boolean

Equal1<1 | 2, 1>
= 1 | 2 extends 1 ? (1 extends 1 | 2 ? true : false) : false;
= 1 | 2 extends 1 ? true : false;
= (1 extends 1 ? true : false) | (2 extends 1 ? true : false); // 분배법칙 적용!
= true | false
= boolean

따라서 명제1으로서는 타입스크립트에서 두 타입이 서로 일치하는지 확인할 수 없습니다.

명제2

두 집합 AA,BB 가 있을 때, 임의의 집합 CC에 대해 아래의 두 조건이 성립하면 AABB는 동일한 집합이다

  1. CCAA의 부분집합이면 CCBB의 부분집합이다.
  2. CCAA의 부분집합이 아니라면 CCBB의 부분집합이 아니다.
(증명)

임의의 집합 CC에 대해, CCAA의 부분집합일 때 CCBB의 부분집합이며, CCAA의 부분집합이 아닐 때 CCBB의 부분집합도 아니라도, AABB가 서로 상이한 집합이라고 가정해보겠습니다.

AABB가 서로 상이한 집합이라고 한다면, AA에는 속하지만 BB에는 속하지 않는 원소가 1개 이상 존재하며, 이러한 원소들을 원소로 하는 집합 D가 존재합니다.

DD의 모든 원소들은 AA에 속하므로 DDAA의 부분집합이지만, DD의 모든 원소들은 BB에는 속하지 않으므로 BB의 부분집합이 아닙니다.

그러나 이는 임의의 집합 CC에 대해 CCAA의 부분집합일 때 CCBB의 부분집합이라는 가정에 어긋나므로, AABB는 서로 동일한 집합입니다.


명제2는 명제1과 다르게 타입스크립트로 그대로 옮기기가 쉽지 않습니다. 명제2를 조금더 자세히 살펴보겠습니다.

임의의 집합 CC

임의의 집합 CCAA의 부분집합인 C1C_1AA의 부분집합이 아닌 C2C_2로 구분할 수 있습니다. (C=C1C2)(C = C_1 \lor C_2)

마찬가지로 임의의 집합 CCBB의 부분집합인 C3C_3BB의 부분집합이 아닌 C4C_4 로 구분할 수 있습니다. (C=C3C4)(C = C_3 \lor C_4)

  1. CCAA의 부분집합이면 CCBB의 부분집합이다.

이는 CCC1C_1이면 C3C_3이다와 동일하며,

  1. CCAA의 부분집합이 아니라면 CCBB의 부분집합이 아니다.

이는 CCC2C_2이면 C4C_4이다와 동일합니다.

따라서 명제2가 성립한다는 것은 C1C_1C3C_3가 일치하고, C2C_2C4C_4가 일치한다는 것과 동일합니다.

보조타입 F<T>의 정의

함수 표현식의 제네릭 타입과 extends 키워드를 이용해서 CCTT의 부분집합이면 1을 반환하고 CCTT의 부분집합이 아니면 0을 반환하는 함수를 정의해보겠습니다.

type F<T> = <C>() => C extends T ? 1 : 0;

F<A>F<B>의 서브타입이면 명제2가 성립한다

F<A>F<B>의 서브타입이기 위해서는,

  1. F<B>1을 반환할 때 F<A>1을 반환해야 하고,

  2. F<B>0을 반환할 때는 F<A>0을 반환해야 합니다.

(인자는 없으므로 고려대상이 아닙니다1)

F<A>C1C_1, C2C_2를 나타낸다면 C1C_1() => 1 을, C2C_2() => 0 이 될 것입니다.

또한 F<B>C3C_3, C4C_4를 나타낸다면 C3C_3() => 1 을, C4C_4() => 0 이 될 것입니다.

이를 이용하면 서브타입이기 위한 조건을 집합 관계로 나타낼 수 있습니다.

  1. F<B>1을 반환할 때 F<A>1을 반환해야 하고,

F<B>C3C_3일 때 F<A>C1C_1 이여야 하고,

  1. F<B>0을 반환할 때는 F<A>0을 반환해야 합니다.

F<B>C4C_4 일 때 F<A>C2C_2 여야 함을 의미하므로,

'F<A>F<B>의 서브타입이다' 와 'C1C_1C3C_3이 일치하고 C2C_2C4C_4이 일치한다' 가 동일한 의미임을 알 수 있습니다.

즉, F<A>F<B>의 서브타입인 것과 명제2가 성립한다는 것은 동일한 표현입니다.

따라서 이를 이용해서 두 타입이 서로 일치하는 지 확인하는 함수 Equal2를 정의할 수 있습니다.

type F<T> = <C>() => C extends T ? 1 : 0;

type Equal2<A, B> = F<A> extends F<B> ? true : false;

테스트 결과

declare const a: Equal2<boolean, false>; // false
declare const b: Equal2<false, false>; // true
declare const c: Equal2<true | false, boolean>; // true
declare const d: Equal2<never, never>; // true
declare const e: Equal2<1 | 2 | 3, 2 | 3>; // false
declare const f: Equal2<3 | 2, 2 | 3>; // true

Footnotes

  1. 💡 두 함수타입 X, Y 가 있을때 X가 Y의 서브타입이려면
    1) X의 인자가 Y의 인자의 슈퍼타입이고 (반공변)
    2) X의 반환값이 X의 반환값의 서브타입이어야 합니다. (공변)