TypeScript 유틸리티 타입 실무 활용 — Partial, Pick, Omit, Record 완벽 정리
TypeScript 유틸리티 타입은 기존 타입에서 새 타입을 파생시키는 내장 도구입니다. Partial은 모든 필드를 선택적으로, Pick은 필요한 필드만, Omit은 특정 필드를 제외, Record는 키-값 맵을 만듭니다. 각각을 언제 쓰는지와 실무 조합 패턴을 예제로 정리합니다. (TypeScript 5.x 기준)

사진: Unsplash의 Luca Bravo
Partial — 모든 프로퍼티를 선택적으로
Partial<T>는 타입 T의 모든 프로퍼티를 ?(optional)로 바꿔줍니다. PATCH 요청처럼 일부 필드만 수정할 때 유용합니다.
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
// PATCH /users/:id — 일부 필드만 수정
async function updateUser(id: number, data: Partial<User>): Promise<User> {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
return response.json();
}
// 컴파일 에러 없이 일부 필드만 전달 가능
await updateUser(1, { name: '홍길동' });
await updateUser(1, { email: 'new@example.com', role: 'admin' });
주의: Partial은 얕게(shallow) 적용됩니다. 중첩 객체까지 optional로 만들려면 재귀 유틸리티 타입(DeepPartial)을 별도로 정의해야 합니다.
Pick — 필요한 프로퍼티만 추출
Pick<T, K>는 타입 T에서 키 K만 골라 새 타입을 만듭니다. API 응답 타입을 뷰 전용 DTO로 축소할 때 자주 쓰입니다.
interface Article {
id: number;
title: string;
content: string;
authorId: number;
createdAt: Date;
updatedAt: Date;
tags: string[];
}
// 목록 화면에는 일부 필드만 필요
type ArticleListItem = Pick<Article, 'id' | 'title' | 'createdAt' | 'tags'>;
function ArticleCard({ article }: { article: ArticleListItem }) {
return (
<div>
<h2>{article.title}</h2>
<time>{article.createdAt.toLocaleDateString()}</time>
</div>
);
}
content나 authorId 같은 무거운 필드가 컴포넌트로 흘러들어 오는 것을 타입 수준에서 차단할 수 있습니다.
Omit — 특정 프로퍼티만 제외
Omit<T, K>는 Pick의 반대로, K를 제거한 나머지 타입을 만듭니다. DB에서 가져온 엔티티를 외부에 노출할 때 민감 필드를 제거하는 패턴에 적합합니다.
interface UserEntity {
id: number;
name: string;
email: string;
passwordHash: string; // 절대 외부 노출 금지
createdAt: Date;
}
// API 응답 타입 — passwordHash 제외
type UserResponse = Omit<UserEntity, 'passwordHash'>;
function toUserResponse(user: UserEntity): UserResponse {
const { passwordHash, ...rest } = user;
return rest;
}
유틸리티 타입 한눈에 보기
| Partial<T> | 모든 프로퍼티를 optional로 변환 | PATCH DTO, 폼 초기값 |
| Required<T> | 모든 프로퍼티를 필수로 변환 | 옵션 객체 완전 검증 |
| Pick<T, K> | K만 남긴 서브타입 | 뷰 DTO, 목록 아이템 |
| Omit<T, K> | K를 제거한 서브타입 | 민감 필드 제거, 응답 타입 |
| Record<K, V> | K를 키, V를 값으로 하는 맵 타입 | 코드 테이블, 캐시 맵 |
| Readonly<T> | 모든 프로퍼티를 읽기 전용으로 | 불변 상수 객체 |
Record — 키-값 맵 타입 정의
Record<K, V>는 키 K와 값 V로 구성된 객체 타입을 만듭니다. 유니온 타입을 키로 사용하면 누락 케이스를 컴파일 타임에 잡을 수 있습니다.
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
// 각 상태에 대한 한국어 레이블 — 새 상태 추가 시 여기도 반드시 추가해야 컴파일 통과
const STATUS_LABEL: Record<OrderStatus, string> = {
pending: '결제 대기',
processing: '처리 중',
shipped: '배송 중',
delivered: '배송 완료',
cancelled: '주문 취소',
};
// 상태별 색상 매핑
const STATUS_COLOR: Record<OrderStatus, 'gray' | 'blue' | 'orange' | 'green' | 'red'> = {
pending: 'gray',
processing: 'blue',
shipped: 'orange',
delivered: 'green',
cancelled: 'red',
};
OrderStatus에 새 값을 추가하면 Record 객체에도 해당 키를 추가하지 않으면 컴파일 에러가 발생합니다. 이 방식으로 실수로 케이스를 빠뜨리는 버그를 원천 차단할 수 있습니다.
조합 패턴 — 실무에서 유틸리티 타입 섞어 쓰기
유틸리티 타입은 중첩해서 쓸 때 진가가 드러납니다.
interface Product {
id: number;
sku: string;
name: string;
price: number;
stock: number;
description: string;
imageUrl: string;
}
// 생성 시 id는 서버 자동 생성 — Omit으로 제거
type CreateProductDto = Omit<Product, 'id'>;
// 수정 시 id는 필수, 나머지는 선택적 — Pick + Partial 조합
type UpdateProductDto = Pick<Product, 'id'> & Partial<Omit<Product, 'id'>>;
// 목록용 경량 타입
type ProductSummary = Pick<Product, 'id' | 'sku' | 'name' | 'price' | 'stock'>;
// 재고 현황 맵: SKU → 재고 수량
type StockMap = Record<Product['sku'], Product['stock']>;
Product['sku']처럼 인덱스드 액세스 타입과 Record를 조합하면 필드 타입 변경 시 자동으로 StockMap도 갱신됩니다.
정리
| Partial<T> | PATCH DTO, 폼 상태처럼 일부 필드만 필요할 때 |
| Pick<T, K> | 뷰 DTO, 목록 아이템처럼 서브셋이 필요할 때 |
| Omit<T, K> | 민감 필드 제거, 생성 DTO에서 자동 생성 필드 제외할 때 |
| Record<K, V> | 유니온 키로 완전한 맵 타입을 강제할 때 |
| 중첩 조합 | Pick<X, K> & Partial<Omit<X, K>> 패턴으로 정교한 DTO 구성 |
결론
TypeScript 유틸리티 타입은 타입을 복사·붙여넣기 하지 않고 원본에서 파생시키는 핵심 도구입니다. Partial, Pick, Omit, Record만 숙달해도 DTO 설계와 컴포넌트 props 타입 정의가 훨씬 깔끔해집니다. 원본 타입이 바뀔 때 파생 타입도 자동으로 따라오므로, 타입 불일치 버그를 컴파일 타임에 잡을 수 있습니다.
자주 묻는 질문 (FAQ)
Q. Pick과 Omit 중 뭘 써야 하나요?
남길 필드가 더 적으면 Pick, 제외할 필드가 더 적으면 Omit이 읽기 좋습니다. 결과 타입이 같아도 의도가 드러납니다.
Q. Partial과 Required의 차이는?
Partial<T>는 모든 프로퍼티를 optional(?)로, Required<T>는 모든 프로퍼티를 필수로 바꿉니다. 서로 반대입니다.
Q. Record와 인덱스 시그니처( { [k: string]: V } )는 뭐가 다른가요?
Record<K, V>는 K에 유니온 리터럴을 주면 키가 전부 있는지 컴파일 타임에 강제합니다. 인덱스 시그니처는 임의 키를 허용해 누락을 못 잡습니다.
📚 참고 출처 (2026년 7월 19일 확인)
· TypeScript Handbook — Utility Types
메인 이미지: 사진 Luca Bravo (Unsplash)

COMMENTS