CATEGORY

카테고리 (674)
AI (72)
Language & Specs (260)
FrameWork (36)
Library (20)
App (41)
Git (10)
Build & Dependency (2)
AWS (15)
DataBase (45)
OS (33)
Tool (17)
IT (120)
SEEMINGLY ONLINE

Seemingly
Online

이모저모 방방곡곡 두루두루 개발지식 저장소

RECENT POSTS

Language & Specs/TypeScript

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에 유니온 리터럴을 주면 키가 전부 있는지 컴파일 타임에 강제합니다. 인덱스 시그니처는 임의 키를 허용해 누락을 못 잡습니다.

💡 타입 할당 에러로 막혔다면 TypeScript bigint를 ReactNode에 할당할 수 없는 문제 해결법도 함께 참고하세요.

📚 참고 출처 (2026년 7월 19일 확인)
· TypeScript Handbook — Utility Types

메인 이미지: 사진 Luca Bravo (Unsplash)

반응형

COMMENTS