개요

Record<Key, Value>는 키와 값 타입을 고정해 객체 형태를 만드는 타입스크립트 유틸리티 타입임 인덱스 시그니처와 유사하지만 문자열 리터럴 유니온을 키로 직접 사용할 수 있고, 맵드 타입으로도 같은 효과를 낼 수 있음

핵심 개념과 정의

  • Record<Key, Value>
    • 키 타입 Key, 값 타입 Value를 갖는 객체 타입 생성
  • 인덱스 시그니처와의 차이
    • [key: string]: T 형태는 키 집합을 특정 리터럴 유니온으로 제한 불가
    • Record는 ‘A’ | ‘B’ 같은 리터럴 유니온을 키로 직접 지정 가능
  • 맵드 타입과의 관계
    • { [K in Keys]: V }와 Record<Keys, V>는 구조적으로 동일한 결과를 만들 수 있음

간단 스니펫

type Names = '치즈볼' | '초코볼'

// 인덱스 시그니처는 리터럴 유니온으로 키 제한 불가
// type Score = { [name: '치즈볼' | '초코볼']: number } // 불가

// 맵드 타입
type Score = { [K in Names]: number }

// Record
type ScoreRecord = Record<Names, number>

동작 원리와 구조

  • 키 집합 정의
    • 리터럴 유니온을 직접 지정하거나 keyof T로 도출
  • 옵셔널 전파 여부
    • 맵드 타입 { [K in keyof T]: V }는 기본적으로 T의 속성 옵셔널 여부를 전파함
    • Record<keyof T, V>는 원본 T의 옵셔널 여부를 전파하지 않고 모든 키를 필수로 간주함

옵셔널 차이 스니펫

interface CountType {
  views?: number
  likes?: number
}

// 맵드 타입은 옵셔널 전파 가능성 존재
const a: { [K in keyof CountType]: number[] } = { views: [], likes: [] }
// a.views 사용 시 possibly 'undefined' 경고 가능

// Record는 모든 키를 필수로 간주
const b: Record<keyof CountType, number[]> = { views: [], likes: [] }
// b.views 접근 시 undefined 경고 없음

적용기 요약

프론트 개발에서 한 인터페이스의 키 집합을 다른 타입에도 그대로 재사용할 일이 많았음 초기에는 다음처럼 맵드 타입을 인라인으로 사용

const chartData: { [K in keyof CountType]: ChartType[] } = { /* ... */ }

이 타입을 별도로 분리하려 했으나 인터페이스로는 맵드 타입만으로 정의 불가

// interface X { [K in Keys]: V } 형태로 순수 맵드 타입만으로 인터페이스 선언 불가

대안

  • 속성을 한 번 감싸 인터페이스 내부에 맵드 타입을 둠
    • 단, 실제 사용 시 X[‘data’]처럼 내부 프로퍼티로 접근해야 하는 번거로움 발생
  • type 별칭으로 맵드 타입 선언
    • 팀 규칙상 interface 중심 사용 시 도입이 어려울 수 있음

실전 선택은 Record 활용

const chartData: Record<keyof CountType, ChartType[]> = { /* ... */ }

타입을 완전히 분리하지는 않지만 선언이 짧아지고 가독성이 좋아짐 필요하면 type 별칭으로 Record를 감싸 재사용 가능

type ChartDataType = Record<keyof CountType, ChartType[]>

언제 무엇을 쓸까

  • 리터럴 유니온 키를 간단히 객체 타입으로 만들고 싶음
    • Record 선호
  • 기존 타입 T의 키 집합에 값을 매핑하고, 옵셔널이나 readonly 같은 수정자 전파가 필요함
    • 맵드 타입 선호
  • 팀 컨벤션이 interface 중심이고 맵드 타입을 인터페이스로 직접 선언해야 한다는 요구가 있음
    • 불가하므로 Record 인라인 사용 또는 type 별칭 최소 도입 고려
  • 키 집합이 바뀔 때 컴파일 타임에 누락을 잡고 싶음
    • Record와 맵드 타입 모두 keyof를 사용하면 누락 감지 가능

간단 예시

리터럴 유니온 키를 가진 점수판 타입 정의

type Names = '치즈볼' | '초코볼'
type Scores = Record<Names, number>

주의 사항

  • 맵드 타입은 기본적으로 원본의 옵셔널 여부를 전파하므로 접근 시 undefined 가드 필요 가능
  • Record<keyof T, V>는 모든 키를 필수로 보고 생성하므로 값 초기화 전략을 먼저 정리할 것
  • interface로는 순수 맵드 타입을 직접 선언할 수 없음
  • 공용 타입을 재사용한다면 이름 있는 type 별칭으로 감싸 관리하는 편이 안전

마무리

Record는 인덱스 시그니처보다 명시적 키 집합을 다루기 쉽고, 맵드 타입보다 선언을 짧게 유지하기 좋음 옵셔널 전파 여부와 팀 컨벤션을 고려해 Record와 맵드 타입을 상황에 맞게 선택하는 것이 핵심임 유틸리티 타입 전반을 익혀두면 리팩터링과 타입 재사용성 모두에서 이점을 얻을 수 있음

참고자료