TypeScript Record 타입으로 타입 안전한 딕셔너리 만들기

문제

API 응답 코드별 메시지를 객체로 관리하고 있는데, 새 코드를 추가할 때 빼먹어도 TypeScript가 잡아주지 않는다. { [key: string]: string } 같은 인덱스 시그니처는 너무 느슨하다.

해결

Record<K, V>를 쓰면 키와 값을 모두 타입으로 강제할 수 있다.

type StatusCode = 'success' | 'error' | 'pending' | 'timeout';

const statusMessages: Record<StatusCode, string> = {
  success: '완료되었습니다',
  error: '오류가 발생했습니다',
  pending: '처리 중입니다',
  timeout: '시간이 초과되었습니다',
};
// 하나라도 빠지면 컴파일 에러

StatusCode에 새 값을 추가하면 statusMessages에서 바로 에러가 난다. 빼먹을 수가 없는 거다.

enum 키와 조합하면 더 강력하다.

enum Permission {
  Read = 'read',
  Write = 'write',
  Delete = 'delete',
}

const permissionLabels: Record<Permission, string> = {
  [Permission.Read]: '읽기',
  [Permission.Write]: '쓰기',
  [Permission.Delete]: '삭제',
};

모든 키를 강제하고 싶지 않을 때는 Partial과 조합한다.

// 일부만 정의해도 OK
const overrides: Partial<Record<StatusCode, string>> = {
  error: '서버 에러입니다',
};

중첩 객체에도 유용하다.

type Locale = 'ko' | 'en' | 'ja';

const translations: Record<Locale, Record<string, string>> = {
  ko: { greeting: '안녕하세요' },
  en: { greeting: 'Hello' },
  ja: { greeting: 'こんにちは' },
};

핵심 포인트

  • Record<K, V>는 K의 모든 키에 대해 V 타입 값을 강제한다
  • 유니온 타입이나 enum을 키로 쓰면 빠뜨린 키를 컴파일 타임에 잡아준다
  • 일부만 필요하면 Partial<Record<K, V>>를 쓴다
  • { [key: string]: V } 인덱스 시그니처보다 훨씬 정밀하다
  • 설정 매핑, i18n, 상태 관리 등에서 자주 쓰인다