Ayden's journal

Equal but not Equal

TypeScript에서 두 개의 타입이 겉보기에는 동일하지만, 실제로 비교하면 다르게 평가되는 경우가 있다. 예를 들어, 다음 두 타입을 비교해보자.

type A = { x: string, y: string };
type B = { x: string } & { y: string };

 

두 타입 A와 B는 직관적으로 같아 보인다. 하지만 TypeScript는 { x: string, y: string }과 { x: string } & { y: string }을 동일한 타입으로 간주하지 않는다. 이를 확인하기 위해 두 타입이 완전히 동일한지 판별하는 유틸리티 타입 IsEqual을 만들어보자.

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

type Result = IsEqual<A, B>; // Result는 false

 

겉보기에는 true가 나올 것 같지만, 실제로 false가 나온다. 즉, TypeScript는 두 타입이 완전히 동일하지 않다고 판단한다. 일반적인 TypeScript 상식으로는 A와 B가 동일한 타입처럼 보이지만, 실제 타입 검사에서는 "직접 선언된 객체 타입"과 "인터섹션을 통해 합쳐진 타입"이 동일하지 않을 수 있다.

{ x: string, y: string }은 명시적인 객체 타입이고, { x: string } & { y: string }은 인터섹션을 통해 생성된 타입이다. TypeScript는 이 둘을 내부적으로 다르게 평가하기에 IsEqual<A, B>는 false를 반환하게 된다.

 

이전까지 나는 타입 A와 타입 B가 상호 호환 가능하다면, 이 두 타입이 '동일하다'고 생각했다. 그러나 TypeScript에서는 상호 호환 가능해도 이들이 완전히 동일한 타입은 아닐 수 있다는 사실을 알게 되었다. 타입 간의 호환성은 타입이 어떻게 정의되었는지에 따라 달라지며, 구조적 타입 시스템에서는 명시적으로 선언된 타입과 인터섹션을 통해 결합된 타입이 같은 구조를 가졌다고 해도 내부적인 평가 차이로 인해 동일하지 않게 간주될 수 있다.

type K = A extends B ? true : false // true
type L = B extends A ? true : false // true
type R = IsEqual<A, B> // false

 

따라서 타입을 비교할 때 상호 호환성만으로 동일성을 판단해서는 안 된다. TypeScript의 타입 시스템은 구조적 타이핑을 기반으로 작동하지만, 인터섹션 타입과 객체 타입은 그 평가 방식에서 미세한 차이를 보일 수 있다는 점을 염두에 두어야 한다.

 

 

+ A와 B를 동일하게 만들기 위해서는 이전에 소개했던 유틸리티 타입 Roll을 사용해서 인터섹션을 명시적 객체로 바꿔버리는 방법이 있겠다.

type Result = IsEqual<A, Roll<B>>; // Result는 true

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기