Equal but not Equal

작성일:2025.03.11|조회수:0

Equal but not Equal

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

다음 두 타입을 살펴보자.

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

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

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

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

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

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를 반환한다.

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

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

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

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

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

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

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

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

더 읽어보기

  • 2025.09.14

    Loose Autocomplete

    프로그래밍을 하다 보면 개발자가 의도한 선택지를 에디터가 자동완성으로 얼마나 잘 안내해 주느냐가 개발 경험에 큰 영향을 미친다는 사실을 자주 체감하게 된다. 특히 문자열 기반의 옵션을 다룰 때는 이 차이가 더욱 극명하게 드러난다.예를 들어 HTTP 메서드를 표현할 때 "GET" | "P…

  • 2025.04.25

    더 좁은 타입의 유효성에 대하여

    사람이 무언가를 집중해서 바라보다 보면, 어느샌가 주변부가 흐려지고 가끔은 집중하고 있던 그 대상조차 보이지 않게 된다. 처음엔 분명하게 인식되던 경계가 서서히 사라지고, 오히려 애써 무시했던 주변이 본질을 가릴 때도 있다. 잘 보려 애쓰는 행위가, 역설적으로 시야를 좁히는 순간이다.개…

  • 2025.04.06

    재귀 조건부 타입에서의 추론 컨텍스트 손실

    타입스크립트에서는 조건부 타입의 분배 과정에서도 타입 추론 컨텍스트가 유지된다. 이 특성 덕분에 단순한 분기 수준을 넘어, 상당히 복잡한 조건부 타입에서도 개발자가 의도한 방향으로 타입 추론을 유도할 수 있다. 실제로 이러한 특성은 고급 유틸리티 타입을 설계할 때 매우 강력한 도구로 작…

  • 2025.03.28

    유틸리티 타입 IsInRange

    프론트엔드 개발을 하다 보면 컴포넌트의 props로 number 타입을 받을 때가 많다. 하지만 단순히 number 타입만 지정하면 값의 범위를 제한할 수 없다는 점이 아쉽다. 예를 들어, 페이지네이션의 currentPage는 1 이상의 값만 허용해야 하고, 슬라이더의 value는 최소…

  • 2026.04.11

    Trie 자료구조

    문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…

  • 2026.03.19

    Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까

    웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…

댓글

댓글을 불러오는 중...