TypeScript의 조건부 타입(Conditional Types)은 T extends U ? X : Y 원리에 따라 하나의 타입을 다른 타입으로 설명할 수 있게 해줍니다. 이러한 타입은 복잡한 타입 논리를 구축할 때 강력한 가능성을 제공하며, 특히 라이브러리 및 API 선언에서 유용합니다.
기본 조건부 타입 예시:
type IsString<T> = T extends string ? 'yes' : 'no'; type A = IsString<string>; // 'yes' type B = IsString<number>; // 'no'
조건부 타입이 유니온 타입에 적용되면 TypeScript는 조건을 각 유니온의 요소에 개별적으로 ‘분배’합니다. 이를 조건부 타입의 배급적 행동이라고 부릅니다.
예제:
type Foo<T> = T extends { id: number } ? string : boolean; type Result = Foo<{id: number} | {name: string}>; // string | boolean
이것은 매우 강력한 기능이지만, 특히 배열 및 타입 매핑을 다룰 때 예상되는 결과와의 혼란을 야기할 수 있습니다.
타입을 튜플로 감싸기:
type NoDistrib<T> = [T] extends [{id: number}] ? string : boolean; type Result = NoDistrib<{id: number} | {name: string}>; // boolean
질문: "유니온 타입과 함께 조건부 타입을 사용할 때, 튜플로 감싸지 않으면 어떤 일이 발생하나요? 결과가 항상 일반적인 논리와 동일한가요?"
답변: 결과는 예기치 않게 나올 수 있습니다! 배급성 때문에 조건이 각 유니온 타입의 구성원에 개별적으로 적용됩니다. 전체 유니온 타입을 엄격하게 비교하려면 래퍼(튜플)를 사용해야 합니다.
예제:
type Test<T> = T extends string ? number : boolean; type A = Test<string | boolean>; // number | boolean, 단순히 boolean이 아님
이야기
직렬화 라이브러리에서 데이터 구조를 확인하기 위해 조건부 타입을 사용했지만 제너릭 매개변수를 튜플로 감싸는 것을 잊었습니다. 결과적으로 복잡한 유니온 타입의 선언이 깨지고, 컴파일러는 API 사용 시 예측할 수 없는 타입을 생성했습니다.
이야기
모델 필드를 처리하기 위한 타입 변형을 구현하려 할 때, 배급성 때문에 일부 정보가 손실되었습니다. 배급성으로 인해 유니온이 생성되면서 몇 가지 로직 분기가 처리되지 않았고 결국 타이핑이 지나치게 허용적으로 되었습니다.
이야기
개발자는 T extends SomeType이 조건부 타입 내에서 전체 객체에 대해 우리가 기대하는 대로 작동할 것이라고 생각했지만, "분산"이 발생했습니다. 컴파일러는 불일치를 지적했고, 타입 혼란과 자동 문서화의 심각한 버그가 발생했습니다.