Record<K, V>는 키 타입 칸 K와 값 타입 칸 V를 채워 'K에 속하는 키마다 V 타입의 값을 가지는 객체 타입'을 한 줄로 만드는 제네릭 유틸리티 타입입니다. K에 string을 넣으면 키가 정해지지 않은 객체 — 인덱스 시그니처와 같은 뜻이 되고, 'tall' | 'grande' 같은 리터럴 유니언을 넣으면 모든 키가 빠짐없이 들어 있는지 검사기가 확인해 줍니다.
크기('tall', 'grande')마다 가격을 적은 표를 만들 때, 키를 넓은 Record<string, number>로 적으면 실수로 grande 가격을 빼먹어도 검사기가 모르고 넘어갑니다. 반대로 키가 미리 정해지지 않은 판매 수 집계 표에는 아무 글자 키나 들어갈 수 있어야 합니다. 이 레슨에서는 Record의 첫째 칸 K에 무엇을 넣느냐에 따라 달라지는 두 가지 모양을 비교합니다.
1정해진 키 모음 — Record<Size, number>
리터럴 유니언 type Size = 'tall' | 'grande'를 첫째 칸에 넣어 Record<Size, number>라고 적으면, tall: number와 grande: number 두 줄을 펼쳐 놓은 객체 타입이 됩니다(검사 규칙 4 · 7).
type Size = 'tall' | 'grande';
const bySize: Record<
Size,
number
> = {
tall: 4000,
grande: 4500,
};
console.log(bySize.grande);
4500따라서 Size에 있는 키 가운데 하나라도 빼먹으면 검사기가 즉시 TS2741로 잡아 줍니다.
type Size = 'tall' | 'grande';
const bySize: Record<
Size,
number
> = {
tall: 4000,
};
main.ts(2,7): error TS2741: Property 'grande' is missing in type '{ tall: number; }' but required in type 'Record<Size, number>'.나중에 Size 유니언에 | 'venti'를 더하면, Record<Size, number>를 쓴 모든 객체에서 venti 줄이 빠졌다고 알려 주어 빠뜨린 곳을 한눈에 찾을 수 있습니다.
이제 6단원 직접 만드는 확인 함수에서 'item' in v를 지난 뒤 나왔던 칩을 끝까지 읽을 수 있습니다. 앞쪽 object &(타입에 줄 더하기 — extends와 &)는 '원래 객체이면서 동시에', 뒤쪽 Record<"item", unknown>은 '키가 "item"이고 값 타입이 unknown인 속성 줄을 꼭 가진 객체'라는 뜻입니다.
2열린 string 키 — Record<string, number>와 없는 키 꺼내기
첫째 칸에 리터럴 유니언 대신 string을 넣은 Record<string, number>는 3단원 키가 정해지지 않은 객체 — 인덱스 시그니처에서 본 { [key: string]: number }를 짧게 적은 것과 완전히 같은 타입입니다.
const prices: Record<
string,
number
> = {
커피: 4000,
라테: 4500,
};
const p = prices['빙수'];
console.log(p);
위 코드는 검사를 지나갈까요? 8번 줄에서 p의 지금 타입은 무엇이고, Node로 돌리면 무엇이 찍힐까요? 답을 정한 뒤 펼쳐 보세요.
검사 결과와 실행 결과 보기
undefined예상과 다르면 그곳이 배울 곳입니다.
Record<string, number>는 어떤 글자 키든 받아들이므로 빈 객체 {}나 한두 줄만 있는 객체도 검사를 지납니다.
또한 과목 기본 설정("strict": true)에서 8번 줄 prices['빙수']의 지금 타입은 number라 검사를 지나지만, 실제로는 '빙수' 칸이 없어 실행하면 undefined가 찍힙니다.
인덱스 시그니처 레슨에서 봤듯이, Record<string, number>에서 없을 수도 있는 키를 꺼내 계산에 쓸 때는 prices['빙수'] ?? 0처럼 기본값을 정해 씁니다(또는 10단원 strict가 켜는 것들에서 배우는 noUncheckedIndexedAccess 옵션을 켜면 p의 지금 타입이 number | undefined가 되어 확인 없이 쓰는 줄을 TS18048로 막아 줍니다).
| 구분 | Record<Size, number> (Size = 'tall' | 'grande') | Record<string, number> |
|---|---|---|
| 같은 뜻의 모양 | { tall: number; grande: number } | { [key: string]: number } |
| 키 하나가 빠지면 | TS2741 오류로 막음 | 아무 오류 없음 (빈 객체 {}도 통과) |
| 없는 키를 읽으면 | bySize['short'] → TS7053 오류 | 기본 설정에서 타입은 number, 실행은 undefined |
예제
검사 결과와 실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
요일('mon' | 'tue')마다 목표 걸음 수를 적은 goal과, 실제로 걸은 날만 기록한 walked를 비교합니다.
type Day = 'mon' | 'tue';
const goal: Record<
Day,
number
> = {
mon: 8000,
tue: 8000,
};
const walked: Record<
string,
number
> = {
mon: 9000,
};
const tue = walked['tue'] ?? 0;
console.log(
goal.tue - tue,
);
검사를 지나갈까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
8000예상과 다르면 그곳이 배울 곳입니다.
goal은 mon과 tue 두 줄을 모두 갖추어 Record<Day, number> 검사를 지납니다. walked는 Record<string, number>라 mon 한 줄만 있어도 지나가고, 없는 키 'tue'는 ?? 0으로 받아 8000 - 0인 8000이 찍힙니다.
자주 틀리는 곳
틀린 생각: Record<string, number>로 선언한 객체에서 꺼낸 값은 타입이 number이니 늘 숫자가 들어 있다.
편집기에서 const p = prices['빙수'];에 마우스를 올리면 number라고 나오니, p + 500을 해도 안전할 것처럼 보입니다(꺼낸 값은 늘 있다는 생각).
const prices: Record<
string,
number
> = {
라테: 4500,
};
const p = prices['빙수'];
console.log(p + 500);
NaN맞는 규칙: Record<string, number>는 인덱스 시그니처와 같아서 '키가 있다면 값은 number'라는 뜻일 뿐, 모든 글자 키가 실제로 들어 있다고 보장하지 않습니다. 없을 수 있는 키는 ?? 0이나 !== undefined로 걸러 쓰거나, tsconfig.json에서 noUncheckedIndexedAccess를 켜서(TS18048) 검사기가 잡게 합니다(2절 · strict가 켜는 것들).
틀린 생각: Record<string, number>와 Record<Size, number>는 둘 다 글자 키에 숫자 값을 담으니 똑같이 검사한다.
'tall'과 'grande'도 글자이니 string과 다를 바 없어 보입니다.
맞는 규칙: 첫째 칸에 Size 같은 리터럴 유니언을 넣으면 유니언의 모든 키가 빠짐없이 있는지 검사하고(TS2741), string을 넣으면 어떤 키든 몇 개든 상관없이 허용합니다(2절 표). 키 목록이 정해져 있을 때는 늘 리터럴 유니언을 넣습니다.
틀린 생각: Record는 자바스크립트의 new Map()처럼 실행할 때 새 객체를 만들어 주는 함수이다.
이름이 대문자로 시작하니 const r = Record('a', 1);처럼 값 자리에서 부를 수 있을 것 같습니다(타입이 실행 때 남는다는 생각).
const r = Record('a', 1);
main.ts(1,11): error TS2693: 'Record' only refers to a type, but is being used as a value here.맞는 규칙: Record<K, V>는 타입 자리에만 적는 유틸리티 타입이며 실행 전에 지워집니다(검사 규칙 1). 실제 객체 값은 {} 리터럴로 만듭니다.
확인 문제
1. 다음 코드는 검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 어떤 오류가 날까요?
type Grade = 'A' | 'B' | 'C';
const bonus: Record<
Grade,
number
> = {
A: 100,
B: 50,
};
힌트
Grade 유니언에 들어 있는 세 글자가 bonus 객체에 모두 있는지 확인합니다(1절).
정답
main.ts(2,7): error TS2741: Property 'C' is missing in type '{ A: number; B: number; }' but required in type 'Record<Grade, number>'.2번 줄에서 TS2741로 막힙니다. Record<Grade, number>는 A, B, C 세 키를 모두 요구하는데 C가 빠졌습니다.
흔한 오답은 '지나간다'입니다. Record<string, number>와 헷갈려 일부 키만 적어도 된다고 본 것입니다.
2. 과목 이름(아무 문자열)을 키로 하고 점수(number)를 값으로 하는 객체 타입을 Record로 적으려고 합니다. 2번 줄 빈칸을 채우세요.
const scores: Record<
______________
> = {};
scores['수학'] = 95;
scores['영어'] = 88;
console.log(scores['수학']);
힌트
키 타입 칸과 값 타입 칸을 쉼표로 나란히 적습니다(2절).
정답
빈칸은 string, number입니다.
const scores: Record<
string,
number
> = {};
scores['수학'] = 95;
scores['영어'] = 88;
console.log(scores['수학']);95다르게 써도 되는 곳: interface 안에 [subject: string]: number; 인덱스 시그니처를 선언해 써도 같은 타입입니다.
흔한 오답은 차례를 바꾼 number, string이거나, 값 타입만 적은 number입니다(Record는 두 칸 K, V를 모두 채워야 합니다).
3. 직접 만드는 확인 함수에서 typeof v === 'object'와 v !== null, 그리고 'title' in v까지 확인한 직후, 검사기가 보는 v의 지금 타입은 앞쪽 object &에 뒤쪽 Record<"title", unknown>이 붙은 모양입니다. 이 타입이 뜻하는 바로 가장 알맞은 것을 고르세요.
- ①
v는 객체이고'title'이라는 속성이 들어 있지만, 그 속성 값의 타입은 아직 무엇인지 모른다(unknown). - ②
v는'title'속성만 가지고 다른 속성은 절대 없는 객체이며,'title'의 값은 문자열이다. - ③ 실행할 때
v가 자동으로Record클래스의 인스턴스로 변환된다.
힌트
1절 끝에서 object &와 Record<"title", unknown> 두 조각을 어떻게 나누어 읽었는지 떠올려 보세요.
정답
①입니다. 'title' in v로 'title' 키가 있다는 것은 확인했으므로 Record<"title", unknown> 줄이 더해졌고, 그 안의 값이 문자열인지 숫자인지는 아직 확인하기 전이라 값 타입 칸이 unknown으로 채워져 있습니다.
- ② 값의 타입이 문자열이 되려면
typeof v.title === 'string'까지 확인해야 하며, 다른 속성이 더 있는 객체도 들어올 수 있습니다(검사 규칙 4). - ③
Record는 타입일 뿐 실행 때 클래스를 만들거나 값을 변환하지 않습니다(검사 규칙 1).
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
2절의 prices['빙수'] 코드를 main.ts에 저장하고, 같은 폴더의 tsconfig.json 안에 noUncheckedIndexedAccess 옵션을 true로 켜는 줄을 추가한 뒤 npx tsc를 돌려 p + 500이 TS18048('p' is possibly 'undefined')로 잡히는지 확인해 보세요(확인이 끝나면 추가한 옵션 줄은 다시 지워 둡니다 — 자세한 설정은 10단원 strict가 켜는 것들에서 다룹니다). 다음은 단원 연습 문제와 단원 1~8 섞어 풀기입니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.