main

Equal 타입 분석하기

8

타입 챌린지를 하던 도중, Equal 타입을 마주해서 Equal을 분석하는 글을 포스팅합니다.
Equal타입 같은 경우에는 완벽한 정의가 아니고, 대부분의 상황에 적합한 것이기 때문에 현재 유틸리티 타입에는 포함되지 않는 것 같습니다.🥲


export type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

제가 마주했던 Equal타입은 위와 같은데요, 타입은 말그대로 X와 Y 타입이 같은지 비교하는 타입입니다.
일단 첫번째로 의문점이 들었던 것은 왜 X와 Y를 따로 나눠서 타입을 비교하고 있는 것인지에 대해 의문점이 들었습니다.

📌 1단계

먼저 저는 단순하게 아래와 같이 기입하여 타입을 비교하면 안되나? 라고 생각했습니다.

export type Equal<X, Y> = X extends Y ? true : false;
 
type A = Equal<number, number | string>; // true
type B = Equal<number | string, number>; // boolean

타입 A같은 경우에는 true로 리턴되지만, B같은 경우에는 왼쪽이 유니언타입이기 때문에 분배법칙으로 아래와 같이 하나씩 뜯어서 비교하게 됩니다.

type C = Equal<number, number>
type D = Equal<string, number>

따라서 true, false 값 둘다 모두 가질 수 있기 때문에 boolean으로 타입을 지정하게 됩니다.
하지만 우리가 원하는 것은 둘이 타입이 같은지입니다. 또한 위의 간단한 Equal 타입정의는 서브타입의 반대경우에는 증명할 수 없습니다.

type A = Equal<{a: 3}, {a: 3, b: 5}>; // false
type B = Equal<{a:3, b: 5}, {a: 3}>; // true

객체에서는 더 구체적인게 타입이 좁다고 이해해야합니다. 또한 이 둘은 우리가 원하는대로 모두 false가 나와야합니다. 두개의 객체가 완전히 같지 않기 때문입니다. 따라서 간단한 Equal타입의 정의는 완벽하지 않다고 볼 수 있습니다.

📌 2단계

export type Equal<X, Y> = X extends Y ? (Y extends X ? true : false) : false;

그렇다면 서로 비교하면서 타입을 체크하면 되지 않을까? 라고 생각하고 위와 같은 타입으로 지정할 수 있습니다.
위의 타입을 기반으로 비교를 한다면 객체의 타입 비교는 완벽하게 하는 것을 확인할 수 있습니다.

type a1 = Equal<{ a: 3 }, { a: 3; b: 5 }>; // false
type a2 = Equal<{ a: 3; b: 5 }, { a: 3 }>; // false
type a3 = Equal<{ a: 3; b: 5 }, { a: 3; b: 5 }>; // true
type a4 = Equal<{ b: 5; a: 3 }, { a: 3; b: 5 }>; // true

그렇다면 이 타입을 다른 타입비교에도 사용할 수 있을까요?

📌 3단계

type a5 = Equal<1 | 2, 1>; // boolean
type a6 = Equal<1, 1 | 2>; // boolean

아직 우리에게는 유니온 타입이라는 문제가 남아 있습니다.
이 두 타입은 누가 봐도 틀렸건만, false가 아닌 boolean을 반환하기 때문입니다.

위 타입을 분석해본다면 아래와 같습니다.

type a5Anda6-1 = 1 | 2 extends 1 ? true : false; // false
type a5Anda6-2 = 1 extends 1 | 2 ? true : false; // true

위의 a5a6은 위와 같은 값 판별을 하게 됩니다. 결론적으로 truefalse를 반복해서 내뱉게 되기때문에 결론적으로는 둘다 false이지만, Equal<1, 1 | 2> 에서는 boolean 타입으로 결과를 내뱉게 됩니다.

최종적으로 나와야 하는 타입은 false인데 왜 boolean이 나오게 된 걸까요?
우리는 전자의 타입인 1 extends 1 | 2를 보고 바로 true라는 것을 알지만, 컴파일러에게는 그저 1 | 2 일 뿐입니다.

따라서 뒤의 식은 true | false 타입이기 때문에 boolean으로 나오는 것입니다.

이를 해결하기 위해서는 조건부 타입을 사용해야 합니다.
이미 우리는 extends 문을 사용하고 있지만, extends 문과 조건부 타입의 사용은 별개의 것입니다.
extends 문에 타입 파라미터나 연산자를 사용해, 조건에 따라 다른 타입을 반환하게 해야만 조건부 타입입니다.
조건부 타입을 쓰면 일반적인 유니온 타입의 분배 법칙을 따라 가는 대신 각 조건의 결과 타입을 따라갑니다.
즉, 앞에서 true를 반환했으면 그 타입이 뒤의 식으로 이어져서 최종적인 타입을 추론하는 데 영향을 준다.

쉽게 말하자면, 각 조건부에 따라서 정확히 반환할 타입을 명확히 기입해주어야한다는 것입니다.

📌 4단계

export type Equal<X, Y> = (<T>() => T extends X ? T: false) extends <T>() => T extends Y ? T : false ?  true : false;

위 타입은 각 조건부에 따라 반환해야할 타입은 truefalse로 정확히 명시해주고 있습니다.

📌 5단계

export type Equal<X, Y> =
  (<T>() => T extends X ? T : 2) extends
  (<P>() => P extends Y ? P : 2) ? true : false
export type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<P>() => P extends Y ? 1 : 2) ? true : false
export type FirstExpression<X> = <T>() => T extends X ? 1 : 2;
export type SecondExpression<Y> = <P>() => P extends Y ? 1 : 2;
export type Equal<X, Y> = FirstExpression<X> extends SecondExpression<Y> ? true : false;

Reference Doc

Equal type 설명하기


김다은 이모지
Daeun Kim
Junior Frontend Engineer