PUBLISHED

Equal but not Equal

작성일: 2025.03.11

Equal but not Equal

TypeScript를 사용하다 보면, 겉보기에는 동일해 보이지만 실제로는 다르게 평가되는 타입을 마주치는 경우가 있다. 특히 객체 타입과 인터섹션 타입을 함께 다룰 때 이런 차이가 분명하게 드러난다.

다음 두 타입을 살펴보자.

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

직관적으로 보면 AB는 완전히 같은 구조를 가진 타입이다. 두 타입 모두 xy라는 문자열 프로퍼티를 갖고 있고, 실사용 관점에서도 서로를 대체해 사용할 수 있다. 하지만 TypeScript는 이 두 타입을 완전히 동일한 타입으로 간주하지 않는다.

이를 확인하기 위해, 두 타입이 동일한지를 판별하는 유틸리티 타입 IsEqual을 만들어보자.

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

이제 AB를 비교해보면 다음과 같은 결과가 나온다.

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

겉보기에는 true가 나올 것 같지만, 실제 결과는 false다. 즉, TypeScript는 { x: string; y: string }{ x: string } & { y: string }서로 다른 타입으로 판단하고 있다.

이 차이는 타입의 “구조”가 아니라, 타입이 만들어진 방식에서 비롯된다. A는 하나의 명시적인 객체 타입으로 선언된 반면, B는 두 객체 타입을 인터섹션을 통해 결합한 타입이다. TypeScript는 이 둘을 내부적으로 동일한 형태로 정규화하지 않으며, 그 결과 타입 비교 단계에서는 서로 다른 타입으로 취급된다.

흥미로운 점은, 두 타입이 서로 완전히 호환 가능하다는 사실이다. 양방향으로 extends가 성립한다는 것은, 실질적인 사용 관점에서는 두 타입이 동일하게 동작한다는 의미다. 그럼에도 불구하고, IsEqual<A, B>는 여전히 false를 반환한다.

untitled
TS
type K = A extends B ? true : false; // true
type L = B extends A ? true : false; // true

이 결과는 TypeScript에서 타입 호환성과 타입 동일성은 서로 다른 개념임을 분명하게 보여준다. 구조적 타입 시스템을 사용한다고 해서, 구조가 같으면 언제나 동일한 타입으로 평가되는 것은 아니다.

이전까지 나는 두 타입이 상호 호환 가능하다면, 그 둘은 사실상 “같은 타입”이라고 생각해왔다. 하지만 TypeScript의 타입 시스템에서는 그 생각이 항상 옳지 않다. 타입은 단순히 어떤 형태를 가지는지가 아니라, 어떤 과정을 통해 정의되었는지에 따라서도 다르게 취급될 수 있다.

이 차이는 보통 일상적인 타입 사용에서는 크게 문제되지 않는다. 하지만 타입 동일성을 기준으로 분기하는 유틸리티 타입을 작성하거나, 라이브러리 레벨에서 타입을 조합하고 비교하는 경우에는 예상치 못한 결과를 만들어낼 수 있다. 특히 인터섹션 타입을 적극적으로 사용하는 코드베이스라면, 이 미묘한 차이를 인지하고 있어야 한다.

다행히 이 문제를 완화하는 방법은 있다. 이전에 소개했던 Roll 유틸리티 타입을 사용하면, 인터섹션 타입을 명시적인 객체 타입으로 한 번 정규화할 수 있다.

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

Roll<B>는 인터섹션으로 구성된 타입을 하나의 객체 타입처럼 재평가하기 때문에, A와의 비교에서 true를 반환하게 된다. 이를 통해 타입의 “형태”뿐만 아니라, 비교 가능한 동일성까지 맞추는 것이 가능해진다.

결국 이 사례가 보여주는 핵심은 명확하다. TypeScript에서 타입을 다룰 때는 “쓸 수 있는가”와 “같은가”를 구분해야 한다. 상호 호환성만으로 타입의 동일성을 판단하는 것은, 특히 타입 레벨 로직을 작성하는 상황에서는 충분하지 않을 수 있다.