[TypeScript] Mapped Types로 타입 재사용하기

Mapped Types의 기본: 속성 변환

Mapped Types는 주로 인덱스 시그니처 문법을 활용하여 기존 타입의 모든 속성을 대상으로 새로운 타입을 생성합니다.

interface AppState {
  loading: number;
  error: string;
}

type BooleanState<T> = {
  [K in keyof T]: boolean;
};

type AppFlags = BooleanState<AppState>;

const flags: AppFlags = {
  loading: false,
  error: true,
};

console.log(flags); // 출력: { loading: false, error: true }

매핑 수정자: 속성 제어

`+`와 `-`로 `readonly`나 선택적 속성(`?`)을 추가하거나 제거할 수 있습니다.

readonly 추가

interface Config {
  apiKey: string;
  timeout: number;
}

type ReadonlyConfig<T> = {
  readonly [K in keyof T]: T[K];
};

type LockedConfig = ReadonlyConfig<Config>;

const config: LockedConfig = { apiKey: "xyz", timeout: 5000 };
// config.apiKey = "new"; // 오류: 읽기 전용
console.log(config.apiKey); // 출력: xyz

선택적 속성 제거

interface PartialItem {
  name?: string;
  price?: number;
}

type RequiredItem<T> = {
  [K in keyof T]-?: T[K];
};

type FixedItem = RequiredItem<PartialItem>;

const item: FixedItem = { name: "책", price: 15000 };
// const incomplete: FixedItem = { name: "책" }; // 오류: price 누락
console.log(item); // 출력: { name: "책", price: 15000 }

키 리맵핑: as로 이름 재정의

TypeScript 4.1부터는 as 키워드를 사용하여 기존 키를 새 이름으로 리맵핑할 수 있습니다.

interface Profile {
  email: string;
  age: number;
}

type SetterProfile<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

type ProfileSetters = SetterProfile<Profile>;

const setters: ProfileSetters = {
  setEmail: (value) => console.log(`Email set to ${value}`),
  setAge: (value) => console.log(`Age set to ${value}`),
};

setters.setEmail("a@b.com"); // 출력: Email set to a@b.com
setters.setAge(25);          // 출력: Age set to 25

조건부 타입 결합하기

조건부 타입과 함께 사용하면 특정 속성만 선택하거나 제외할 수 있습니다.

interface Data {
  value: string;
  meta: { created: string };
}

type WithoutMeta<T> = {
  [K in keyof T as Exclude<K, "meta">]: T[K];
};

type SimpleData = WithoutMeta<Data>;

const data: SimpleData = { value: "정보" };
console.log(data.value); // 출력: 정보

유니온 타입과의 조합

Mapped Types는 유니언 타입에서도 분배되어 동작합니다.

type Alert = { type: "info"; message: string } | { type: "error"; code: number };

type AlertHandler<T extends { type: string }> = {
  [E in T as E["type"]]: (alert: E) => string;
};

type Handlers = AlertHandler<Alert>;

const alertHandlers: Handlers = {
  info: (alert) => `정보: ${alert.message}`,
  error: (alert) => `오류 ${alert.code}`,
};

console.log(alertHandlers.info({ type: "info", message: "완료" })); // 출력: 정보: 완료
console.log(alertHandlers.error({ type: "error", code: 404 }));    // 출력: 오류 404