TypeScript Conditional Types로 타입 안전성 높이기

문제 상황

프로젝트에서 API 응답 형태가 성공/실패에 따라 달랐다. 기존에는 Union Type으로만 처리했는데, 타입 가드를 매번 작성하는 게 번거로웠다.

type ApiResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

function handleResponse<T>(response: ApiResponse<T>) {
  if (response.success) {
    // response.data 접근 가능하지만 타입 가드 필요
  }
}

Conditional Types 적용

TypeScript 2.8부터 지원하는 Conditional Types를 활용해 개선했다.

type ApiResult<T, S extends boolean> = S extends true
  ? { success: true; data: T }
  : { success: false; error: string };

function fetchUser<S extends boolean>(
  id: string,
  shouldSucceed: S
): Promise<ApiResult<User, S>> {
  // 구현
}

// 사용
const result = await fetchUser('123', true);
// result는 자동으로 { success: true; data: User } 타입

실전 활용: Extract와 Exclude

유틸리티 타입들도 내부적으로 Conditional Types를 사용한다는 걸 알게 됐다.

type EventType = 'click' | 'scroll' | 'mousemove';
type ClickEvent = Extract<EventType, 'click'>; // 'click'
type NonClickEvents = Exclude<EventType, 'click'>; // 'scroll' | 'mousemove'

// 실제 적용 예시
type FormFieldValue<T extends string> = T extends 'checkbox'
  ? boolean
  : T extends 'number'
  ? number
  : string;

type CheckboxValue = FormFieldValue<'checkbox'>; // boolean
type TextValue = FormFieldValue<'text'>; // string

배운 점

Conditional Types는 제네릭과 결합하면 강력하다. 다만 너무 복잡한 타입은 가독성을 해치므로, 재사용성이 높은 경우에만 사용하는 게 좋겠다. 팀원들과 코드 리뷰하면서 적절한 추상화 수준을 찾아가는 중이다.

TypeScript Conditional Types로 타입 안전성 높이기