
Typescript를 사용하는 가장 큰 이유는 강력한 타입 시스템을 바탕으로 안정성과 가독성을 높여 효율적으로 작업하기 위함일 것입니다.
이를 위해 Typescript 자체적으로 제공하는 Utility Type들이 있는데 이번 글에서 소개할 것은 그 중에서 유용성에 비해 주목을 덜 받고있다고 생각하는 Record 타입입니다.
Record는 한마디로 객체의 키와 밸류의 타입을 정의하는 유틸리티 타입입니다.
Record의 기본 형식은 Record<Keys, Type> 으로 Keys가 키로 사용될 타입이며, Type은 그 키에 연결될 밸류의 타입입니다.
Keys에 다양한 타입이 들어갈 수 있지만 실질적으로 string 혹은 number의 리터럴 타입을 쓰게 되는 경우가 가장 많고 또한 효율적입니다.
Type에는 어떠한 타입도 사용할 수 있습니다.
Typescript Docs의 Record 사용 예시는 다음과 같은데 사실 처음 보면 굳이 Record 타입을 사용해야 할 이유를 공감 못할 사람이 더 많을 것입니다.
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
const borisCatInfo = cats.boris; // CatInfo
솔직히 지금 다시 봐도 저걸 실제로 어떤 상황에 쓰라는건지 감이 안오는데, 그래도 일단 분석해보면
cats의 type을 Record<CatName, CatInfo>라 정의했는데 이는 이 Object의 key는 CatName으로 하고 value는 CatInfo type이라는 걸 명시한 것입니다.
Record<CatName, CatInfo>를 interface로 변환하면 다음과 같습니다.
interface CatNameInfo {
miffy: CatInfo,
boris: CatInfo,
mordred: CatInfo,
}
const cats_interface: CatNameInfo = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
}
그럼 타입을 interface와 Record로 정의했을 때, 어떤 차이가 있는지 살펴봅시다.
고양이 한 마리가 더 생겨서 현재 3가지인 CatName 타입에 하나가 추가되었다고 하죠.
type CatName = "miffy" | "boris" | "mordred" | "neo"; // neo 추가
interface CatNameInfo {
miffy: CatInfo,
boris: CatInfo,
mordred: CatInfo,
}
// ✅ OK
const cats_interface: CatNameInfo = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
}
// ❌ Property 'neo' is missing in type
const cats_record: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
CatName에 neo가 추가되었지만 CatNameInfo interface에는 추가되지 않았으므로 interface로 타입을 지정한 cats_interface는 여전히 3 고양이의 정보를 가지고 있습니다.
하지만 Record 타입의 cats_record는 새로 추가된 neo에 해당하는 밸류가 없다고 즉시 타입 오류를 내뱉는 것을 볼 수 있습니다.
Record가 타입 안정성과 관련되었다는것이 이런 의미입니다.
티를 내지 않는 interface와 달리 컴파일 단계에서 key가 누락된 것을 확인할 수 있기 때문에 어떤 객체에 어떤 key가 필요한 지 즉시 알 수 있고 반영할 수 있게 합니다.
고양이도 좋지만 조금 더 실용적인 예시를 살펴봅시다.
만만한 Todo list에서 Todo의 진행 상태에 따라 count 하는 코드입니다.
Typescript Playground <= 직접 실행할 수 있습니다.
type TodoStatus = 'TODO' | 'PROGRESS' | 'DONE';
interface Todo {
title: string,
status: TodoStatus,
description: string,
}
// Record type 정의
type TodoStatusCount = Record<TodoStatus, number>;
// initialize 함수
const emptyTodoStatusCount = (): TodoStatusCount => ({ TODO: 0, PROGRESS: 0, DONE: 0 });
// todo를 status에 따라 count
const getTodoStatusCount = (todos: Todo[]) => todos.reduce<TodoStatusCount>(
(statusCount, todo) => {
statusCount[todo.status] += 1;
return statusCount;
},
emptyTodoStatusCount()
);
//
const todos: Todo[] = [
{ title: 'Todo 1', status: 'DONE', description: '1 - Done' },
{ title: 'Todo 2', status: 'PROGRESS', description: '2 - Progress' },
{ title: 'Todo 3', status: 'TODO', description: '3 - Todo' },
{ title: 'Todo 4', status: 'PROGRESS', description: '4 - Progress' },
];
console.log(getTodoStatusCount(todos)); // { "TODO": 1, "PROGRESS": 2, "DONE": 1 }