타입스크립트에서 두 타입이 일치한다는 건 어떻게 알 수 있을까요? 타입이란 할당 가능한 값의 집합이기 때문에 두 타입이 일치한다는 것은 각 타입에 할당 가능한 값의 집합이 서로 일치한다는 것과 동일한 의미입니다. 아래에서는 두 집합이 일치하는지 확인하는 방법을 통해 두 타입이 일치하는 지 확인해보도록 하겠습니다.
명제1
두 집합 와 가 있을 때, 가 의 부분집합이고 가 의 부분집합이면, 와 는 동일한 집합이다
(증명)
가 의 부분집합이고 가 의 부분집합일 때, 와 가 서로 다른 집합이라고 가정해보겠습니다.
그렇다면 에는 속하지만 에는 속하지 않는 원소 가 존재합니다.
그러나 는 의 부분집합이므로 에 속하는 임의의 원소는 모두 에 속하게 되므로, 원소 는 존재할 수 없습니다.
따라서 가정이 잘못되었으므로, 가 의 부분집합이고 가 의 부분집합이면, 와 는 동일한 집합임을 알 수 있습니다.
타입스크립트에서 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
두 집합 , 가 있을 때, 임의의 집합 에 대해 아래의 두 조건이 성립하면 와 는 동일한 집합이다
- 가 의 부분집합이면 는 의 부분집합이다.
- 가 의 부분집합이 아니라면 는 의 부분집합이 아니다.
(증명)
임의의 집합 에 대해, 가 의 부분집합일 때 가 의 부분집합이며, 가 의 부분집합이 아닐 때 가 의 부분집합도 아니라도, 와 가 서로 상이한 집합이라고 가정해보겠습니다.
와 가 서로 상이한 집합이라고 한다면, 에는 속하지만 에는 속하지 않는 원소가 1개 이상 존재하며, 이러한 원소들을 원소로 하는 집합 D가 존재합니다.
의 모든 원소들은 에 속하므로 는 의 부분집합이지만, 의 모든 원소들은 에는 속하지 않으므로 의 부분집합이 아닙니다.
그러나 이는 임의의 집합 에 대해 가 의 부분집합일 때 가 의 부분집합이라는 가정에 어긋나므로, 와 는 서로 동일한 집합입니다.
명제2는 명제1과 다르게 타입스크립트로 그대로 옮기기가 쉽지 않습니다. 명제2를 조금더 자세히 살펴보겠습니다.
임의의 집합
임의의 집합 는 의 부분집합인 과 의 부분집합이 아닌 로 구분할 수 있습니다.
마찬가지로 임의의 집합 를 의 부분집합인 와 의 부분집합이 아닌 로 구분할 수 있습니다.
- 가 의 부분집합이면 는 의 부분집합이다.
이는 가 이면 이다와 동일하며,
- 가 의 부분집합이 아니라면 는 의 부분집합이 아니다.
이는 가 이면 이다와 동일합니다.
따라서 명제2가 성립한다는 것은 과 가 일치하고, 와 가 일치한다는 것과 동일합니다.
보조타입 F<T>
의 정의
함수 표현식의 제네릭 타입과 extends
키워드를 이용해서 가 의 부분집합이면 1
을 반환하고 가 의 부분집합이 아니면 0
을 반환하는 함수를 정의해보겠습니다.
type F<T> = <C>() => C extends T ? 1 : 0;
F<A>
가 F<B>
의 서브타입이면 명제2가 성립한다
F<A>
가 F<B>
의 서브타입이기 위해서는,
-
F<B>
가1
을 반환할 때F<A>
가1
을 반환해야 하고, -
F<B>
가0
을 반환할 때는F<A>
는0
을 반환해야 합니다.
(인자는 없으므로 고려대상이 아닙니다1)
F<A>
로 , 를 나타낸다면 은 () => 1
을, 는 () => 0
이 될 것입니다.
또한 F<B>
로 , 를 나타낸다면 은 () => 1
을, 는 () => 0
이 될 것입니다.
이를 이용하면 서브타입이기 위한 조건을 집합 관계로 나타낼 수 있습니다.
F<B>
가1
을 반환할 때F<A>
가1
을 반환해야 하고,
F<B>
가 일 때 F<A>
는 이여야 하고,
F<B>
가0
을 반환할 때는F<A>
는0
을 반환해야 합니다.
F<B>
가 일 때 F<A>
는 여야 함을 의미하므로,
'F<A>
가 F<B>
의 서브타입이다' 와 '과 이 일치하고 와 이 일치한다' 가 동일한 의미임을 알 수 있습니다.
즉, 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
-
💡 두 함수타입 X, Y 가 있을때 X가 Y의 서브타입이려면
1) X의 인자가 Y의 인자의 슈퍼타입이고 (반공변)
2) X의 반환값이 X의 반환값의 서브타입이어야 합니다. (공변) ↩