맵드 타입(Mapped Types)과 조건부 타입(Conditional Types)

타입스크립트를 사용하다 보면 비슷한 구조의 인터페이스를 여러 개 만들어야 하거나, 입력된 타입에 따라 결과 타입을 유연하게 결정해야 하는 상황을 마주하게 됩니다.
이 가이드에서는 중복 없는 타입 정의를 위한 맵드 타입과, 타입 시스템에 논리 구조를 부여하는 조건부 타입 및 infer 키워드에 대해 상세히 다룹니다.
1. 맵드 타입 (Mapped Types)
맵드 타입은 자바스크립트 배열의 map 함수처럼, 타입 내의 프로퍼티들을 순회하며 새로운 프로퍼티 타입을 생성하는 문법입니다.
1.1. 기본 문법
type NewType = {
[K in keyof ExistingType]: NewPropertyType;
};
- keyof ExistingType: 기존 타입의 모든 키(Property Names)를 가져옵니다.
- K in ...: 가져온 키들을 하나씩 순회합니다.
1.2. 기초 예제: 모든 필드 변환
모든 값이 boolean인 타입을 string으로 변환하는 예제입니다.
interface AppFeatures {
isDarkMode: boolean;
isLoggedIn: boolean;
}
type FeatureLabels = {
[K in keyof AppFeatures]: string;
};
const labels: FeatureLabels = {
isDarkMode: "다크 모드 활성화",
isLoggedIn: "로그인 상태"
};
2. 매핑 수정자(Mapping Modifiers)
맵드 타입을 정의할 때 프로퍼티 앞에 readonly를 붙이거나, 뒤에 ?를 붙여 속성을 변경할 수 있습니다. - 기호를 사용하면 기존 속성을 제거할 수도 있습니다.
예제: 모든 필드를 필수값으로 변경 (-?)
interface User {
id?: number;
name?: string;
}
// 모든 선택 사항(?)을 제거하여 필수값으로 변경
type RequiredUser = {
[K in keyof User]-?: User[K];
};
3. 조건부 타입 (Conditional Types)
조건부 타입은 타입 시스템 내에서 '조건문'을 사용할 수 있게 해줍니다. 기본적인 형태는 삼항 연산자와 유사합니다.
3.1. 기본 문법
$$T \text{ extends } U ? X : Y$$
- 타입 $T$가 $U$에 할당 가능하다면 타입은 $X$가 되고, 그렇지 않으면 $Y$가 됩니다.
3.2. 실무 예제: 타입 필터링
특정 타입이 문자열인지 확인하여 타입을 결정하는 로직입니다.
type IsString<T> = T extends string ? "yes" : "no";
type T1 = IsString<string>; // "yes"
type T2 = IsString<number>; // "no"
4. infer 키워드: 타입 추론의 핵심
infer는 조건부 타입의 extends 절에서만 사용할 수 있으며, 런타임이 아닌 컴파일 타임에 타입을 추론하여 변수처럼 사용할 수 있게 합니다.
4.1. 예제: 함수의 반환 타입 추출하기
TypeScript의 내장 유틸리티 타입인 ReturnType<T>은 내부적으로 infer를 사용하여 구현되어 있습니다.
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function getUser() {
return { id: 1, name: "Alice" };
}
// getUser 함수의 반환 타입인 { id: number; name: string; }을 추론해냄
type UserType = MyReturnType<typeof getUser>;
4.2. 예제: 배열의 요소 타입 추출하기
배열 안에 어떤 타입이 들어있는지 뽑아낼 때 유용합니다.
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type StringArray = string[];
type Unpacked = UnpackArray<StringArray>; // string
type NonArray = number;
type NotUnpacked = UnpackArray<NonArray>; // number
5. 실무 종합 예제: API 응답 처리
API 응답 구조에서 실제 데이터(Data) 부분만 골라내거나, Promise가 해결된 후의 타입을 정의할 때 조건부 타입과 infer는 필수적입니다.
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Promise 안의 제네릭 타입을 추출하는 유틸리티
type AwaitedResponse<T> = T extends Promise<ApiResponse<infer D>> ? D : never;
async function fetchUser() {
const response: ApiResponse<{ id: number; name: string }> = {
data: { id: 101, name: "TechBlog" },
status: 200,
message: "Success"
};
return response;
}
// fetchUser가 반환하는 Promise 내부의 data 타입을 가져옴
type RealData = AwaitedResponse<ReturnType<typeof fetchUser>>;
6. 분산 조건부 타입 (Distributive Conditional Types)
조건부 타입에 유니온 타입을 전달하면, 유니온의 각 요소에 조건부 타입이 각각 적용(분산)됩니다.
type ToArray<T> = T extends any ? T[] : never;
// string[] | number[] 로 분산되어 적용됨
type StrOrNumArray = ToArray<string | number>;
요약
TypeScript의 고급 타입들을 마스터하면 더욱 견고한 추상화가 가능해집니다.
- 맵드 타입: 기존 타입의 키를 순회하며 일괄적으로 타입을 변형합니다.
- 조건부 타입: $T \text{ extends } U ? X : Y$ 구조로 조건에 따른 유연한 타입 정의를 지원합니다.
- infer 키워드: 복잡한 타입 구조 속에서 특정 타입을 추론하여 변수처럼 활용합니다.
- 결합 활용: 이 도구들을 조합하여 Pick, Omit, ReturnType과 같은 강력한 유틸리티 타입을 직접 설계할 수 있습니다.
고급 타입을 통해 코드의 중복은 줄이고, 타입 안정성은 극대화하는 설계를 실천해 보세요.