본문 바로가기
language/typescript

⚖️ 조건부 타입 (Conditional Types) 완전 정리

by 죄니안죄니 2025. 4. 27.

⚖️ 조건부 타입 (Conditional Types) 완전 정리

조건부 타입은 타입스크립트에서 타입을 조건에 따라 분기할 수 있는 문법입니다.
코드의 if처럼, 타입 안에서 "A면 B, 아니면 C" 같은 논리를 표현할 수 있습니다.


① 기본 문법

T extends U ? X : Y

- 조건: T가 U를 "확장"하는가? - 참일 때: 타입 X - 거짓일 때: 타입 Y


② 간단한 예제

type IsString = T extends string ? 'Yes' : 'No';

type A = IsString;  // 'Yes'
type B = IsString;  // 'No'

③ 조건부 타입 + 제네릭

함수의 반환 타입, 속성의 타입에 따라 조건을 걸 수 있습니다.

type Flatten = T extends Array<infer U> ? U : T;

type A = Flatten<number[]>; // number
type B = Flatten<string>;    // string

infer를 이용해 배열 내부 타입을 뽑아내는 것도 조건부 타입 활용 예입니다.


④ 분산 조건부 타입

유니언 타입에 조건부 타입을 사용하면 각 타입에 분산 평가가 일어납니다.

type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<string | number>; // string[] | number[]

모든 유니언 멤버에 대해 조건부 타입이 각각 적용됩니다.

type Boxify<T> = T extends any ? T[] : never;

type Box = Boxify<'a' | 1 | true>; // 결과: 'a'[] | 1[] | true[]
// T가 뭐가 오든 그걸 배열로 감싸고 싶을 때. 특히 유니언 타입인 경우, 각 타입마다 배열로 감싸서 반환 (분산 조건부 타입)

//***비교 
type OnlyIfArray<T> = T extends any[] ? T : never;

type A = OnlyIfArray<string[]>;   // string[]
type B = OnlyIfArray<string>;     // never
type C = OnlyIfArray<number[]>;   // number[]
// 배열만 통과시키고 나머지는 제거.

⑤ 실전 예: API 응답 타입 분기

type ApiResponse = {            // ApiResponse<T> API 응답구조 정의
  success: true;
  data: T;
} | {
  success: false;
  error: string;
};
// Apiresponse<T>는 성공했을 때, 실패했을 때 두 가지 중 하나임. 응답의 형태를 정의. 응답의 값은 data에 담음

type ExtractData = T extends { success: true; data: infer D } ? D : never;
// 그 중 성공응답에서 성공 구조를 갖는 경우에만 data값을 추출하는 타입. ExtractData. 응답을 받는 쪽에서 필요. 

type Res = ApiResponse<{ name: string }>;           //실제 응답 예시
type DataOnly = ExtractData<Res>; // { name: string } // data필드의 타입 추출
// Res는 ApiResponse, ExtractData가 제대로 동작하는지 확인하기 위해 샘플 응답타입을 만듦. 
// data 안에 {name:string}이라는 객체가 들어있는 성공/실패 응답 타입을 만들어줘~~~

const response = await api.get<ApiResponse<{ name: string }>>('/user');

// ==================================================================
type GetUserResponse = ApiResponse<{ name: string; age: number }>;

type UserData = ExtractData<GetUserResponse>;
// → { name: string; age: number }

 

✅ 정리해서 말씀드리면

📤 ApiResponse<T> → 응답을 보내는 쪽 (서버 / 백엔드) 에서 사용

function getUser(): ApiResponse<User> {
  return {
    success: true,
    data: { id: 1, name: '홍길동' },
  };
}
  • 서버에서 data와 error 구조를 공통으로 맞추기 위해 사용
  • 여러 API에서 공통으로 쓰기 위해 제네릭 구조로 만듦

📥 ExtractData<T> → 응답을 받는 쪽 (프론트엔드 / 클라이언트) 에서 사용

type Res = ApiResponse<{ name: string }>;

type DataOnly = ExtractData<Res>;
// DataOnly → { name: string }

const response: Res = await api.get('/user');

if (response.success) {
  const user: DataOnly = response.data;
}
  • 클라이언트에서는 API 응답에서 data만 추출해서 사용할 일이 많음
  • 타입 추론을 통해 data 안의 구조를 쉽게 얻기 위해 ExtractData 같은 헬퍼 타입을 사용

💡 왜 이렇게 분리해서 쓰냐면?

구분역할사용 위치
ApiResponse<T> 응답의 포맷을 통일 서버 (API 반환 타입)
ExtractData<T> 성공 시 응답의 data 타입만 추출 클라이언트 (응답 처리용)

✅ 그래서 이런 흐름이 성립합니다:

1. 백엔드에서:
type User = { id: number; name: string };

function getUser(): ApiResponse<User> { ... }
2. 프론트엔드에서:
type Res = ApiResponse<{ id: number; name: string }>;
type User = ExtractData<Res>;

function handleResponse(res: Res) {
  if (res.success) {
    const user: User = res.data; // 안전하고 추론된 타입!
  }
}

📐 요약 정리

형식 설명 예시
T extends U ? X : Y 조건 분기 IsString<T>
infer 타입 추론 변수 infer U
분산 조건부 유니언 각 요소에 분기 적용 T extends any ? T[] : never

🧠 마무리

조건부 타입은 복잡한 타입 로직에서 **타입 추론, 선택적 분기, 타입 재구성** 등 정말 다양한 곳에 사용됩니다.
특히 라이브러리, API 응답 처리, Form 데이터 타입 추출 등에서 유용하게 쓰여요.


📘 다음 글 예고

👉 Enum vs Const Enum 차이 – enum은 왜 성능 이슈가 있을까? const enum은 언제 써야 할까?

 

댓글