TypeScript 유틸리티 타입을 잘 쓰면 타입 중복 없이 유연하고 안전한 코드를 만들 수 있습니다.
TypeScript를 쓰다 보면 비슷한 타입을 조금씩 변형해서 여러 곳에 쓰는 상황이 자주 생깁니다. 이럴 때마다 새 타입을 만들면 코드가 중복되고, 원본 타입이 바뀌면 파생 타입도 일일이 수정해야 합니다. 유틸리티 타입(Utility Types)은 이런 문제를 해결하기 위해 TypeScript가 기본으로 제공하는 타입 변환 도구입니다.
1. Partial<T> — 모든 프로퍼티를 선택적으로
타입의 모든 프로퍼티를 optional(?)로 바꿉니다. 폼 수정, PATCH API 요청 타입에 자주 쓰입니다.
interface User {
id: number
name: string
email: string
}
// User의 모든 필드가 선택적이 됩니다
type UpdateUserDto = Partial<User>
// { id?: number; name?: string; email?: string }
// 실사용 예시: PATCH 요청
async function updateUser(id: number, data: Partial<User>) {
await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
})
}
updateUser(1, { name: '우연' }) // ✅ 일부만 전달해도 OK
updateUser(1, { name: '우연', email: 'a@b.com' }) // ✅
{ [P in keyof T]?: T[P] }로 구현되어 있습니다. 깊은 중첩 객체는 얕게(shallow)만 적용되므로, 중첩된 경우 직접 재귀 타입을 만들거나 DeepPartial 유틸을 쓰는 게 좋습니다.
2. Required<T> — 모든 프로퍼티를 필수로
Partial의 반대로, 모든 optional을 제거해 필수로 만듭니다.
interface Config {
theme?: 'light' | 'dark'
language?: string
timezone?: string
}
// 설정 완료 후 반드시 모든 값이 채워진 상태
type ResolvedConfig = Required<Config>
// { theme: 'light' | 'dark'; language: string; timezone: string }
function applyConfig(config: ResolvedConfig) {
// 이 함수 안에서는 모든 필드가 반드시 존재함이 보장됨
document.documentElement.setAttribute('data-theme', config.theme)
}
3. Pick<T, K> — 필요한 프로퍼티만 골라내기
타입에서 원하는 프로퍼티만 선택해 새 타입을 만듭니다. API 응답에서 일부 필드만 사용하는 컴포넌트에 유용합니다.
interface User {
id: number
name: string
email: string
password: string
createdAt: string
}
// 유저 목록 카드에서는 id, name만 필요
type UserCard = Pick<User, 'id' | 'name'>
// { id: number; name: string }
function UserListItem({ id, name }: UserCard) {
return <li key={id}>{name}</li>
}
// Pick + Partial 조합: 일부 필드만 선택적으로 수정
type PartialUserName = Partial<Pick<User, 'name' | 'email'>>
// { name?: string; email?: string }
4. Omit<T, K> — 특정 프로퍼티 제외하기
Pick의 반대로, 특정 프로퍼티를 제거한 타입을 만듭니다. 민감한 정보를 제거하거나 DB 모델에서 자동생성 필드를 빼는 데 씁니다.
interface User {
id: number
name: string
email: string
password: string
createdAt: string
}
// 생성 요청 DTO: id, createdAt은 서버에서 자동 생성
type CreateUserDto = Omit<User, 'id' | 'createdAt'>
// { name: string; email: string; password: string }
// 클라이언트에 응답할 때 password 제거
type SafeUser = Omit<User, 'password'>
// { id: number; name: string; email: string; createdAt: string }
5. Readonly<T> — 불변 타입 만들기
모든 프로퍼티를 읽기 전용으로 만듭니다. 상태 객체나 설정값이 의도치 않게 변경되는 걸 막을 때 씁니다.
interface Point {
x: number
y: number
}
const origin: Readonly<Point> = { x: 0, y: 0 }
origin.x = 10 // ❌ Error: Cannot assign to 'x' because it is a read-only property
// React에서 props 타입에 Readonly를 적용하면
// 컴포넌트 내부에서 실수로 props를 수정하는 걸 컴파일 단계에서 막을 수 있습니다
type ButtonProps = Readonly<{
label: string
onClick: () => void
}>
6. Record<K, V> — 키-값 맵 타입 만들기
키 타입과 값 타입을 받아 객체 타입을 만듭니다. 딕셔너리 형태의 데이터를 다룰 때 편리합니다.
type Status = 'todo' | 'in-progress' | 'done'
interface Task {
title: string
count: number
}
// 상태별 태스크 집계 객체
const taskByStatus: Record<Status, Task> = {
todo: { title: '할 일', count: 5 },
'in-progress': { title: '진행 중', count: 2 },
done: { title: '완료', count: 12 },
}
// API 응답을 id 기반으로 정규화할 때도 자주 쓰임
type UserMap = Record<number, User>
const usersById: UserMap = {
1: { id: 1, name: '우연', email: 'a@b.com' },
2: { id: 2, name: '클로드', email: 'b@c.com' },
}
7. Exclude<T, U> / Extract<T, U> — 유니온 타입 필터링
유니온 타입에서 특정 타입을 빼거나 추려낼 때 사용합니다.
type AllEvents = 'click' | 'focus' | 'blur' | 'keydown' | 'scroll'
// scroll만 제외
type WithoutScroll = Exclude<AllEvents, 'scroll'>
// 'click' | 'focus' | 'blur' | 'keydown'
// 키보드 관련만 추출
type KeyEvents = Extract<AllEvents, 'keydown' | 'keyup' | 'keypress'>
// 'keydown' (AllEvents에 없는 keyup, keypress는 자동 제외)
// null, undefined 제거에도 자주 쓰임
type NonNullString = Exclude<string | null | undefined, null | undefined>
// string
8. ReturnType<T> — 함수 반환 타입 추출
함수의 반환 타입을 별도 선언 없이 추출합니다. 외부 라이브러리 함수나 자동생성 코드의 반환 타입을 재사용할 때 유용합니다.
function getUser() {
return {
id: 1,
name: '우연',
role: 'admin' as const,
}
}
type UserResponse = ReturnType<typeof getUser>
// { id: number; name: string; role: 'admin' }
// TanStack Query와 조합
import { useQuery } from '@tanstack/react-query'
const useUserQuery = () =>
useQuery({ queryKey: ['user'], queryFn: getUser })
type UserQueryResult = ReturnType<typeof useUserQuery>
// useQuery의 반환 타입 전체를 그대로 가져옴
한눈에 보는 유틸리티 타입 정리
| 유틸리티 타입 | 역할 | 주요 사용 상황 |
|---|---|---|
Partial<T> | 모든 필드 선택적으로 | PATCH DTO, 폼 수정 |
Required<T> | 모든 필드 필수로 | 초기화 완료 후 타입 보장 |
Pick<T, K> | 필드 선택 | 컴포넌트 props 축소 |
Omit<T, K> | 필드 제외 | CREATE DTO, 민감정보 제거 |
Readonly<T> | 읽기 전용 | 상태/설정 불변 보장 |
Record<K, V> | 키-값 맵 | 정규화 데이터, 딕셔너리 |
Exclude<T, U> | 유니온에서 제외 | 이벤트 타입 필터링 |
Extract<T, U> | 유니온에서 추출 | 특정 타입만 골라내기 |
ReturnType<T> | 함수 반환 타입 | 라이브러리 타입 재사용 |
유틸리티 타입을 잘 조합하면 타입 선언 중복을 크게 줄일 수 있습니다. 특히 Omit + Partial, Pick + Readonly 같은 조합은 API DTO 설계나 컴포넌트 props 정의에서 거의 매일 쓰게 됩니다.
다음 글에서는 조건부 타입(Conditional Types)과 infer 키워드를 활용한 고급 타입 추론을 다뤄볼 예정입니다.
'Engineering > Frontend' 카테고리의 다른 글
| React의 <ViewTransition>으로 페이지 전환 애니메이션 만들어보기 (0) | 2026.03.08 |
|---|---|
| React Compiler v1.0과 Next.js 16 Cache Components가 바꾸는 2026 프론트엔드 개발 패러다임 (0) | 2026.03.06 |
| Swagger에서 API 클라이언트 자동생성까지 — orval + TanStack Query 도입기 (0) | 2026.02.18 |
| 2026년 프론트엔드, '서버 중심 리액트'와 'AI 동료'가 표준이 되다 (0) | 2026.01.18 |
| React 19 도입 1년, 이제 useMemo와 useCallback은 놓아줄 때가 됐다 (3) | 2026.01.10 |