Record<K, V>는 키가 K 타입이고 값이 모두 V 타입인 객체를 나타내는 내장 유틸리티 타입입니다. string 키를 쓰면 딕셔너리를, 리터럴 유니언 키를 쓰면 정확히 그 키들을 가져야 하는 객체를 표현합니다.
Record는 타입 시스템에만 존재합니다. 런타임에서 두 객체는 평범한 JavaScript 객체이므로 객체 리터럴, 스프레드, JSON.stringify 등 객체를 받는 모든 곳에서 동작합니다.
문법과 정의
Record<Keys, Value>
Keys는 객체 키가 될 수 있는 것이어야 합니다. string, number, symbol, 문자열이나 숫자 리터럴의 유니언, 템플릿 리터럴 타입입니다. Value는 어떤 타입이든 됩니다. TypeScript 표준 라이브러리의 정의 전체는 매핑된 타입 한 줄입니다.
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any는 가능한 모든 키 타입의 집합인 string | number | symbol입니다. [P in K]: T는 K의 멤버마다 T 타입 속성을 하나씩 만듭니다. 아래의 두 동작이 모두 여기서 나옵니다. string 같은 넓은 K는 인덱스 시그니처(아무 키나 가능)를 만들고, 유니언 K는 멤버마다 필수 속성을 하나씩 만듭니다.
유니언 키: 모든 키가 필수
키가 리터럴의 유니언이면 Record는 그 키를 빠짐없이, 그리고 그 키만 나열해야 합니다. 컴파일러가 체크리스트가 되는 셈입니다.
Status에 "cancelled"를 추가하면 새 상태에 라벨과 색을 줄 때까지 두 객체 모두 컴파일되지 않습니다. 키를 빠뜨리거나 유니언에 없는 키를 추가하면 컴파일 오류입니다.
index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.
문자열 enum을 키 타입으로 써도 똑같이 동작합니다. Record<Color, string>은 enum 멤버마다 항목을 하나씩 요구합니다.
Record<string, T>와 없는 키
string 키에서는 어떤 키든 허용되고, TypeScript는 존재하지 않는 키를 포함해 모든 조회를 T로 타입 지정합니다. 런타임에서 없는 키는 undefined를 줍니다.
가장 흔한 Record 버그입니다. 대처 방법은 세 가지입니다. 읽기 전에 in이나 Object.hasOwn으로 확인하거나, 값을 V | undefined로 선언하거나, 프로젝트의 모든 인덱스 시그니처 읽기에 | undefined를 붙여 주는 noUncheckedIndexedAccess 컴파일러 옵션을 켜세요. 유니언 키를 쓴 Record는 모든 키가 존재한다고 보장되므로 이 문제가 없습니다.
Partial<Record<K, V>>: 일부 키만
키의 유니언을 쓰되 모든 키를 요구하지 않으려면 Record를 Partial로 감쌉니다. 그러면 읽기 결과가 V | undefined가 되어 실제와 맞습니다.
jp처럼 철자가 틀린 키는 여전히 오류이며, 이것이 Record<string, string>보다 나은 점입니다.
Record 순회하기
Object.keys, Object.values, Object.entries 모두 동작합니다. 문제는 키 타입입니다. Object.keys는 string[]을, Object.entries는 [string, V][]를 반환하며, 여러분의 키 유니언을 돌려주지 않습니다.
TypeScript는 일부러 키를 string으로 둡니다. 런타임에 객체는 타입에 적힌 것보다 많은 속성을 가질 수 있으므로, 일반적으로 Plan[]을 약속하는 것은 안전하지 않습니다. seats처럼 리터럴로 직접 만든 객체라면 캐스팅해도 안전합니다.
데이터로 Record 만들기
Record는 배열을 그룹으로 묶거나 인덱싱한 결과로 흔히 나옵니다. Record 타입의 빈 객체에서 시작해 채워 나갑니다.
Book["genre"]는 인터페이스의 유니언을 키 타입으로 재사용하므로, Book에 장르를 추가하면 byGenre가 새 항목을 요구합니다.
Record<string, unknown>과 인터페이스
Record<string, unknown>은 "문자열 키를 가진 어떤 객체"를 나타낼 때 흔히 쓰는 타입입니다. 객체 리터럴과 type 별칭으로 타입을 지정한 값은 받지만, 인터페이스는 거부됩니다.
index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
Index signature for type 'string' is missing in type 'User'.
인터페이스는 선언 병합으로 확장될 수 있으므로 TypeScript는 인터페이스가 인덱스 시그니처에 맞는다고 가정하지 않습니다. 타입 별칭은 다시 열 수 없으므로 type User = { name: string }은 통과합니다. 흔한 해결책은 대신 object를 받거나(여전히 Object.keys를 호출할 수 있습니다), 함수를 제네릭으로 만들거나(<T extends object>(obj: T)), User를 type으로 선언하는 것입니다(다른 차이는 interface와 type 페이지에서 다룹니다).
Record vs 인덱스 시그니처 vs Map
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| 런타임에 존재 | 아니요, 일반 객체 | 아니요, 일반 객체 | 예, 클래스 |
| 고정된 키 집합 | 예, 유니언 K일 때 | 아니요 | 아니요 |
| 키 타입 | string, number, symbol, 리터럴 유니언, 템플릿 패턴 | string, number, symbol, 템플릿 패턴 | 객체를 포함한 모든 것 |
| 없는 키 조회 타입 | V (string 키일 때) | V | get에서 V | undefined |
| 이름 있는 속성과 섞기 | 교차 타입 &로 | 예, 같은 타입 안에서 | 아니요 |
| JSON과 스프레드 | 예 | 예 | 아니요, 먼저 변환 |
| 크기 | Object.keys(r).length | Object.keys(o).length | m.size |
| 잦은 추가와 삭제 | 동작함 | 동작함 | 이를 위해 설계됨 |
키 집합을 알고 있다면 유니언 키의 Record를 고르세요. 모든 키가 있는지 검사해 주는 유일한 선택지입니다. 열린 문자열 키라면 Record<string, V>와 인덱스 시그니처는 서로 바꿔 쓸 수 있고, 많은 코드베이스가 가독성 때문에 Record를 선호합니다. 런타임에 키를 추가하고 삭제하거나, 키가 문자열이 아니거나, 추가 작업 없이 크기와 삽입 순서가 필요하다면 Map을 쓰세요.
자주 묻는 질문
TypeScript에서 Record란 무엇인가요?
Record<K, V>는 키가 K 타입이고 값이 모두 V 타입인 객체를 나타내는 내장 유틸리티 타입입니다. Record<string, number>는 아무 문자열 키에 숫자 값을 가진 객체이고, Record<"en" | "de", string>은 정확히 en과 de 키를 가지며 둘 다 문자열인 객체입니다.
TypeScript에서 Record와 Map의 차이는 무엇인가요?
Record는 일반 JavaScript 객체의 타입이라 객체 리터럴, JSON, 스프레드와 함께 쓸 수 있고 컴파일 후 사라집니다. Map은 get, set, has, size를 가진 런타임 클래스로, 모든 키의 삽입 순서를 유지하고 객체를 포함한 어떤 타입이든 키로 받으며, get은 V | undefined를 반환합니다. 고정된 데이터나 JSON 모양의 데이터에는 Record를, 런타임에 키를 추가하고 삭제한다면 Map을 쓰세요.
Record<string, T>와 { [key: string]: T }의 차이는 무엇인가요?
값에 대해서는 같은 타입입니다. Record<string, T>는 문자열 인덱스 시그니처를 가진 객체 타입으로 펼쳐집니다. 작은 차이가 두 가지 있습니다. 인덱스 시그니처는 이름을 붙일 수 있고 같은 타입 안에 다른 속성과 나란히 둘 수 있으며, keyof Record<string, T>는 string이지만 keyof { [key: string]: T }는 string | number입니다.
TypeScript에서 Record를 어떻게 순회하나요?
키와 값 쌍은 Object.entries(record), 키는 Object.keys, 값은 Object.values로 얻습니다. 런타임에 객체가 추가 키를 가질 수 있으므로 키는 K가 아니라 string으로 돌아옵니다. Record가 알려진 키의 유니언이라면 캐스팅하세요: (Object.keys(r) as Array<keyof typeof r>).
Record에서 일부 키만 필수로 만들려면 어떻게 하나요?
유니언 키를 쓰면 Record<K, V>는 모든 키를 요구합니다. 전부 선택적으로 만들려면 Partial로 감쌉니다: Partial<Record<Lang, string>>. 섞어 쓰려면 교차 타입을 씁니다: Record<"en", string> & Partial<Record<"de" | "fr", string>>는 en을 요구하고 나머지는 허용합니다.