TypeScript Utility Types 정복하기
TypeScript를 사용하다 보면 비슷하지만 조금씩 다른 타입들을 매번 새로 정의해야 하는 상황을 마주합니다. API 응답에는 id가 필수지만, 데이터를 생성하는 요청 폼(Form)에서는 id가 없어야 한다거나, 특정 상태 값 중 일부만 수정하고 싶을 때 말이죠. 이때마다 interface를 복사-붙여넣기 하고 있다면, 당신은 TypeScript의 진정한 힘을 10%도 쓰지 못하고 있는 것입니다.
이 글에서는 TypeScript가 기본으로 제공하는 Utility Types를 활용하여, 중복을 제거하고(DRY), 유지보수하기 쉬우며, 더욱 안전한 타입 시스템을 구축하는 방법을 약 3,000자 분량으로 심도 있게 다룹니다.
1. 기본 변형: 모양 바꾸기 (Partial, Required, Readonly)
1.1 Partial<T>: 모든 것을 선택적으로
가장 흔하게 쓰이는 유틸리티 타입입니다. 기존 타입의 모든 속성을 선택적(Optional, ?)으로 만듭니다. 주로 PATCH 요청(일부 수정)을 처리할 때 유용합니다.
interface User {
id: number;
name: string;
email: string;
}
// ❌ 나쁜 예: 수정용 타입을 별도로 만듦
interface UpdateUserDto {
name?: string;
email?: string;
}
// ✅ 좋은 예: Partial 사용
function updateUser(id: number, fieldsToUpdate: Partial<User>) {
// ...
}
updateUser(1, { email: 'new@example.com' }); // name이 없어도 OK
1.2 Required<T> & Readonly<T>
Required<T>: 반대로 모든 속성을 필수(Required)로 만듭니다. 설정 객체에서 선택적 옵션을 받아 내부 로직에서는 기본값을 채워 필수로 다룰 때 유용합니다.Readonly<T>: 모든 속성을 읽기 전용으로 만듭니다. Redux나 Zustand 같은 상태 관리 라이브러리에서 **불변성(Immutability)**을 강제하고 싶을 때 탁월합니다.
2. 선택과 배제: Pick과 Omit
2.1 Pick<T, K>: 필요한 것만 골라내기
거대한 인터페이스에서 당장 필요한 몇 가지 필드만 가져옵니다.
// User에서 id와 email만으로 이루어진 새 타입을 만듦
type UserSummary = Pick<User, 'id' | 'email'>;
2.2 Omit<T, K>: 불필요한 것 빼기
Pick의 반대입니다. 특정 필드만 제거하고 나머지(Rest)를 남깁니다.
// User에서 민감한 password 필드를 제거
type UserWithoutPassword = Omit<User, 'password'>;
💡 Tip:
Omit은 유니온 타입과 결합될 때 더욱 강력해집니다.
3. 매핑과 조건부 타입: Record, Exclude, Extract
3.1 Record<K, T>: 객체 타입 생성기
특정한 키 집합에 대해 동일한 값 타입을 가지는 객체를 만들 때, 인덱스 시그니처({ [key: string]: ... })보다 훨씬 명시적이고 안전합니다.
type Page = 'home' | 'about' | 'contact';
const navTitles: Record<Page, string> = {
home: '메인',
about: '소개',
contact: '문의', // 하나라도 빠뜨리면 에러 발생!
};
3.2 Exclude<T, U> & Extract<T, U>
유니온 타입(|)을 다룰 때 사용합니다.
Exclude: T에서 U에 할당 가능한 타입을 제거합니다.Extract: T에서 U에 할당 가능한 타입만 추출합니다.
type Status = 'success' | 'clientError' | 'serverError';
type ErrorStatus = Exclude<Status, 'success'>; // 'clientError' | 'serverError'
4. 함수 관련 유틸리티: ReturnType, Parameters
라이브러리를 사용할 때, 라이브러리가 타입을 export 해주지 않아서 난감했던 적이 있나요? 이때 유용한 것이 타입 추론 헬퍼입니다.
4.1 ReturnType<T>
함수의 반환 타입을 추출합니다. Redux의 Action Creator나 React Query의 fetcher 함수 결과값을 타입으로 쓸 때 유용합니다.
function getUser() {
return { id: 1, name: 'Kim', role: 'ADMIN' };
}
// getUser 함수의 반환값을 타입으로 정의
type UserResponse = ReturnType<typeof getUser>;
// { id: number; name: string; role: string; }
4.2 Parameters<T>
함수의 파라미터 타입을 튜플로 추출합니다.
5. 마치며: 제네릭의 마법사가 되자
이 외에도 NonNullable, Awaited(Promise 처리에 필수!) 등 수많은 유틸리티 타입이 있습니다. 이들은 마법이 아니라 TypeScript의 **제네릭(Generics)**과 **맵드 타입(Mapped Types)**으로 구현된 별칭일 뿐입니다.
유틸리티 타입에 익숙해지면, 단순히 타입을 '선언'하는 것을 넘어 타입을 '계산'하고 '조립'하는 단계로 나아갈 수 있습니다. 이것이 바로 코드 베이스가 커져도 유지보수성을 잃지 않는 시니어 개발자의 비결입니다.