kakasoo

Equal type 설명하기 본문

프로그래밍/TypeScript

Equal type 설명하기

카카수(kakasoo) 2023. 3. 5. 20:47
반응형
 

[Feature request]type level equal operator · Issue #27024 · microsoft/TypeScript

Search Terms Type System Equal Suggestion T1 == T2 Use Cases TypeScript type system is highly functional. Type level testing is required. However, we can not easily check type equivalence. I want a...

github.com

글에서 Equals을 어떻게 설명하는지 나와있다.

다만, 완벽한 정의가 아니고, 대부분의 상황에 적합한 것이기 때문에 유틸리티 타입에 포함되지는 않는 것 같다.

 

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

타입스크립트 챌린지의 채점 기준인 Equal 타입은 위처럼 정의되어 있는데 이걸 어떻게 이해해야 할까?

미리 말하건대 완벽한 해설은 일단 불가능하다.

말했다시피 Equal이라는 타입 자체가 완벽하지 않으니, 만약 완벽하다면 기존의 Equal보다 더 나은 타입이 된다.

당연 복잡한 타입이 될 것이고, 이 글에서는 기존의 Equal을 이해하는 데 도움을 주는 게 최종적인 목표다.

 

해설

단계 1

저 답은 다시 잊은 채로 처음부터 Equal 타입을 정의하는 것이 이해가 더 빠를 것이다.

아래와 같이, 생각하는 흐름을 따라가보자.

 

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

가장 기본은 이렇다.

X가 Y라면, true이고 그렇지 않다면 false이다.

하지만 이 경우에는 X가 Y의 서브 타입이므로, 즉 반대의 경우에 대한 케이스는 증명하지 못한다.

따라서 이렇게 바꿔보자.

이렇게 하더라도 대부분의 타입을 증명하는 데에는 문제가 없다.

 

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

하지만 단순히 값이 아닌 객체 타입을 지정하는 경우에는 문제가 된다.

객체부터는 두 타입에 대한 서브 타입이 있을 수 있기 때문에, 한 쪽이 다른 한 쪽에 포함될 수 있기 때문이다.

 

단계 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을 반환하기 때문이다.

 

// X가 1이고 Y가 1 | 2 인 경우

type a7 = 1 extends 1 | 2 ? true : false; // true
type a8 = 1 | 2 extends 1 ? true : false; // false

1 extends 1 | 2 ? (1 | 2 extends 1 ? true : false) : false; // false
  • 1 extends 1 | 2는 true기 때문에 바로 뒤의 식을 타게 된다.
  • 1 | 2 extends 1 ? true : false 는 false다.
  • 그런데 그 값을 대입하기 전 Equal<1, 1 | 2> 에서는 boolean 타입으로 결과를 뱉는다.

 

단계 3. 추가 해설 : 조건부 타입

최종적으로 나와야 하는 타입은 false인데 왜 boolean이 나온 것일까?

우리는 전자의 타입인 1 extends 1 | 2를 보고 바로 true라는 것을 알지만, 컴파일러에게는 그저 1 | 2 일 뿐이다.

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

 

이를 해결하기 위해서는 조건부 타입을 사용해야 한다.

이미 우리는 extends 문을 사용하고 있지만, extends 문과 조건부 타입의 사용은 별개의 것이다.

extends 문에 타입 파라미터나 연산자를 사용해, 조건에 따라 다른 타입을 반환하게 해야만 조건부 타입이다.

조건부 타입을 쓰면 일반적인 유니온 타입의 분배 법칙을 따라 가는 대신 각 조건의 결과 타입을 따라간다.

즉, 앞에서 true를 반환했으면 그 타입이 뒤의 식으로 이어져서 최종적인 타입을 추론하는 데 영향을 준다.

 

단계 4

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

이렇게 두 식을 extends 뿐만 아니라 T를 활용한 조건부 타입으로 변경할 수 있다.

조건부 타입이기 때문에 T가 의미를 가지던 말던, 그 형식만으로도 타입 추론이 다르게 동작하게 된다.

전자의 식에서 T가 X를 추론한 결과인 T와 false를, 뒤로 넘겨야 하기 때문이다.

후자의 식에서도 T과 false를 반환할 것이고, 그 값이 전자와 동일할 경우 true, 아닐 경우 false를 반환한다.

이 결과 값은 합쳐져서 boolean이 되지 않고, 각각의 파라미터 대입에 따라 true 또는 false를 반환한다.

이 전자 식의 타입 파라미터 T와 후자 식의 타입 파라미터인 T는 이름만 같을 뿐 서로 다른 타입이다.

 

단계 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

타입을 더 간단하게 만들기 위해서 이처럼 반환 타입을 1 또는 2로 바꾸고 제너릭 타입 파라미터를 바꿔줄 수 있다.

이 함수의 의미는 X와 Y에 대해서 각각 그 서브 타입에 해당하는 T, P가 존재한다는 가정이다.

이 가정이 성립한다면 각 식은 1을 뱉게 되어 두 타입 X, Y가 같음을 증명한다.

그러면 다음 질문은, 앞에서 반환된 타입 T가 뒤에 식에서 반환된 P와 같을 때 true인 것과,

그냥 1이 반환되고 뒤에 식에서도 1이 반환된 것이 같을 때 true인 게 왜 같냐는 점이다.

 

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;

이는, X의 서브 타입이 존재하고, Y의 서브 타입이 존재하면 결국 X와 Y는 같다는 추론이 된다.

전자의 식을 FirstExpression이라고 하고 후자의 식을 SecondExpression이라고 정의해보자.

우리는 이미 T와 P가 각각 X와 Y의 서브 타입임을 알았다.

이제 Equal도 마찬가지로 FirstExpression 타입이 SecondExpression 타입의 서브 타입인지를 묻는 걸 안다.

따라서 두 타입의 검증은 T와 P과 서로 무관한지를 의미하는 것으로 해석될 수 있다.

다르게 말하면 두 함수 타입이 서로 호환될 수 있는지를 의미한다.

만약 X와 Y가 string과 number로 대입된다면 T와 P는 전혀 무관하기 때문에 false를 반환하게 된다.

 

반응형