Frontend/Typescript

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

미니임 2026. 2. 20. 11:25

타입스크립트를 사용하다 보면 비슷한 구조의 인터페이스를 여러 개 만들어야 하거나, 입력된 타입에 따라 결과 타입을 유연하게 결정해야 하는 상황을 마주하게 됩니다.

이 가이드에서는 중복 없는 타입 정의를 위한 맵드 타입과, 타입 시스템에 논리 구조를 부여하는 조건부 타입 및 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의 고급 타입들을 마스터하면 더욱 견고한 추상화가 가능해집니다.

  1. 맵드 타입: 기존 타입의 키를 순회하며 일괄적으로 타입을 변형합니다.
  2. 조건부 타입: $T \text{ extends } U ? X : Y$ 구조로 조건에 따른 유연한 타입 정의를 지원합니다.
  3. infer 키워드: 복잡한 타입 구조 속에서 특정 타입을 추론하여 변수처럼 활용합니다.
  4. 결합 활용: 이 도구들을 조합하여 Pick, Omit, ReturnType과 같은 강력한 유틸리티 타입을 직접 설계할 수 있습니다.

고급 타입을 통해 코드의 중복은 줄이고, 타입 안정성은 극대화하는 설계를 실천해 보세요.

반응형