iBetter Books
수정

유틸리티 타입 활용하기

레고 블록을 생각해봅시다. 기본 블록을 조립해서 다양한 모양을 만들 수 있습니다. 그런데 레고 세트에는 특수 변환 블록도 들어있습니다. 기존 블록을 붙이면 모양이 바뀌거나 일부가 생략됩니다. 빨간 블록을 변환 블록에 붙이면 파란 블록이 되거나, 블록의 일부만 남기거나, 선택적으로 만들 수 있습니다.

TypeScript의 유틸리티 타입이 바로 이 변환 블록입니다. 기존 타입을 변형해서 새로운 타입을 만드는 도구들입니다. 모두 제네릭으로 구현되어 있습니다.

기준이 되는 타입

예제 전체에서 쓸 기준 타입을 먼저 정의합니다.

// 새 파일: utility-types-base.tsinterface User {  id: number;  name: string;  email: string;  age: number;  role: "admin" | "user" | "guest";}

User 타입을 다양하게 변형해봅시다.

Partial — 모든 속성을 선택적으로

Partial<T>T의 모든 속성을 선택적(?)으로 만듭니다. 업데이트할 때 일부 속성만 바꾸고 싶을 때 유용합니다.

// 새 파일: partial-example.tsinterface User {  id: number;  name: string;  email: string;  age: number;  role: "admin" | "user" | "guest";}// Partial<User>의 타입:// {//   id?: number;//   name?: string;//   email?: string;//   age?: number;//   role?: "admin" | "user" | "guest";// }function updateUser(userId: number, updates: Partial<User>): void {  // updates에는 User의 어떤 속성이든 일부만 들어올 수 있습니다  console.log(`User ${userId} 업데이트:`, updates);}updateUser(1, { name: "Alice Updated" });// name만 업데이트updateUser(2, { age: 31, email: "[email protected]" });// age와 email만 업데이트updateUser(3, {});// 아무것도 업데이트하지 않는 것도 됩니다

내부 원리. TypeScript 표준 라이브러리에서 Partial은 다음과 같이 정의되어 있습니다.

// TypeScript 내장 타입 (직접 쓰지 않아도 됩니다 — 참고용)type Partial<T> = {  [P in keyof T]?: T[P];};

[P in keyof T]T의 모든 키를 순회하고, ?를 붙여 선택적으로 만듭니다. 값 타입은 T[P]로 원래 타입을 유지합니다. 이 문법(맵드 타입)의 상세한 내용은 "실전 TypeScript"에서 다룹니다. 지금은 "모든 속성을 선택적으로 바꿔준다"고 이해하면 충분합니다.

Required — 모든 속성을 필수로

Required<T>는 반대입니다. 선택적 속성들을 모두 필수로 만듭니다.

// 새 파일: required-example.tsinterface UserDraft {  name: string;  email?: string;  age?: number;}// Required<UserDraft>의 타입:// {//   name: string;//   email: string;  // 필수가 됩니다//   age: number;    // 필수가 됩니다// }function submitUserForm(user: Required<UserDraft>): void {  // 모든 필드가 반드시 채워져 있어야 합니다  console.log(`폼 제출: ${user.name}, ${user.email}, ${user.age}`);}submitUserForm({ name: "Alice", email: "[email protected]", age: 30 });// submitUserForm({ name: "Alice" }); // 오류 — email과 age가 필요합니다

Pick — 일부 속성만 선택하기

Pick<T, K>T에서 K에 해당하는 속성만 골라냅니다. KT의 키 유니온입니다.

// 새 파일: pick-example.tsinterface User {  id: number;  name: string;  email: string;  age: number;  role: "admin" | "user" | "guest";}// Pick<User, "id" | "name">의 타입:// {//   id: number;//   name: string;// }type UserSummary = Pick<User, "id" | "name">;function displayUser(user: UserSummary): string {  return `[${user.id}] ${user.name}`;}const summary: UserSummary = { id: 1, name: "Alice" };console.log(displayUser(summary)); // [1] Alice// 목록 표시 — id와 name만 필요합니다const users: UserSummary[] = [  { id: 1, name: "Alice" },  { id: 2, name: "Bob" },  { id: 3, name: "Carol" },];users.forEach((u) => console.log(displayUser(u)));// [1] Alice// [2] Bob// [3] Carol

API 응답에서 필요한 필드만 뽑아서 화면에 표시할 때 자주 씁니다.

Omit — 일부 속성을 제외하기

Omit<T, K>T에서 K에 해당하는 속성을 제거합니다. Pick의 반대입니다.

// 새 파일: omit-example.tsinterface User {  id: number;  name: string;  email: string;  age: number;  role: "admin" | "user" | "guest";}// Omit<User, "id" | "role">의 타입:// {//   name: string;//   email: string;//   age: number;// }type CreateUserInput = Omit<User, "id" | "role">;// 새 사용자를 만들 때는 id(서버 생성)와 role(기본값 사용)을 받지 않습니다function createUser(input: CreateUserInput): User {  return {    ...input,    id: Math.floor(Math.random() * 1000),    role: "user",  // 기본 역할  };}const newUser = createUser({  name: "Dave",  email: "[email protected]",  age: 28,});console.log(newUser);// { name: "Dave", email: "[email protected]", age: 28, id: ..., role: "user" }

Pick vs Omit 선택 기준: 남길 속성이 적으면 Pick, 제거할 속성이 적으면 Omit을 씁니다.

Record — 키-값 타입 만들기

Record<K, V>는 키 타입이 K이고 값 타입이 V인 객체 타입을 만듭니다.

// 새 파일: record-example.ts// 각 역할에 대한 권한 목록type Role = "admin" | "user" | "guest";type Permission = "read" | "write" | "delete";const permissions: Record<Role, Permission[]> = {  admin: ["read", "write", "delete"],  user: ["read", "write"],  guest: ["read"],};function hasPermission(role: Role, permission: Permission): boolean {  return permissions[role].includes(permission);}console.log(hasPermission("admin", "delete")); // trueconsole.log(hasPermission("guest", "write"));  // falseconsole.log(hasPermission("user", "read"));    // true// 캐시 저장소 — 키는 string, 값은 어떤 타입이든type Cache = Record<string, unknown>;const cache: Cache = {};cache["user:1"] = { id: 1, name: "Alice" };cache["config"] = { debug: true };

내부 원리. Record<K, V>는 다음과 같이 정의됩니다.

// TypeScript 내장 타입 (참고용)type Record<K extends keyof any, T> = {  [P in K]: T;};

K extends keyof anyK가 유효한 객체 키 타입(string, number, symbol)이어야 한다는 제약입니다.

유틸리티 타입 조합하기

유틸리티 타입은 조합해서 쓸 수 있습니다.

// 새 파일: utility-combination.tsinterface User {  id: number;  name: string;  email: string;  age: number;  role: "admin" | "user" | "guest";}// 업데이트할 때 — id와 role은 바꿀 수 없고, 나머지는 일부만 변경 가능type UserUpdateInput = Partial<Omit<User, "id" | "role">>;// 결과:// {//   name?: string;//   email?: string;//   age?: number;// }function updateUser(id: number, updates: UserUpdateInput): void {  console.log(`User ${id} 업데이트:`, updates);}updateUser(1, { name: "Alice Updated" });updateUser(2, { age: 31 });updateUser(3, { email: "[email protected]", age: 25 });// updateUser(4, { role: "admin" }); // 오류 — role은 바꿀 수 없습니다

Partial<Omit<User, "id" | "role">>은 "User에서 id와 role을 빼고, 나머지를 전부 선택적으로"입니다. 실무에서 자주 보이는 패턴입니다.

더 알아두면 좋은 유틸리티 타입

자주 쓰이는 다른 유틸리티 타입들도 있습니다.

// 새 파일: more-utility-types.ts// Readonly — 모든 속성을 읽기 전용으로interface Config {  host: string;  port: number;}const config: Readonly<Config> = { host: "localhost", port: 3000 };// config.host = "example.com"; // 오류 — 읽기 전용입니다// ReturnType — 함수의 반환 타입 추출function getUserInfo() {  return { name: "Alice", age: 30, active: true };}type UserInfo = ReturnType<typeof getUserInfo>;// UserInfo의 타입은 { name: string; age: number; active: boolean }const info: UserInfo = { name: "Bob", age: 25, active: false };// Parameters — 함수의 매개변수 타입 추출function createUser(name: string, age: number, role: string): void {  console.log(name, age, role);}type CreateUserParams = Parameters<typeof createUser>;// CreateUserParams의 타입은 [name: string, age: number, role: string]const params: CreateUserParams = ["Alice", 30, "admin"];createUser(...params);

ReturnTypeParameters는 기존 함수의 타입을 재사용할 때 유용합니다. 함수 시그니처가 바뀌면 이 타입들도 자동으로 업데이트됩니다.

언제 어떤 유틸리티 타입을 쓸까

정리하면 다음과 같습니다.

유틸리티 타입 쓰는 상황
Partial<T> 업데이트 입력, 일부 필드만 채우는 폼
Required<T> 최종 검증 단계, 모든 필드가 채워졌음을 보장
Pick<T, K> 화면 표시용 타입, 필요한 필드만 추출
Omit<T, K> 생성 입력 타입, 서버 생성 필드 제외
Record<K, V> 딕셔너리, 설정 맵, 캐시
Readonly<T> 변경 불가 설정값, 상수 객체
ReturnType<T> 함수 반환 타입을 다른 곳에서 재사용

레고 변환 블록을 사용해봤습니다. User 타입 하나에서 UserSummary, CreateUserInput, UserUpdateInput 등 상황에 맞는 다양한 타입을 만들었습니다. 새 인터페이스를 처음부터 작성하지 않아도 됩니다.

유틸리티 타입들은 모두 제네릭으로 만들어져 있습니다. 앞서 배운 <T>, extends, keyof가 조합된 결과입니다. TypeScript 표준 라이브러리 코드를 보면 이 장에서 배운 내용들이 그대로 쓰이고 있습니다.

다음 장에서는 제네릭 인터페이스와 클래스를 만들어봅니다. API 응답을 감싸는 ApiResponse<T>, 데이터를 관리하는 Repository<T> 같은 실전 패턴을 다룹니다.

Ch 04. 유틸리티 타입 활용하기 — 소설처럼 읽는 TypeScript | iBetter Books