Waylog Blog

TypeScript Utility Types 정복하기

TypeScript

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)**으로 구현된 별칭일 뿐입니다. 유틸리티 타입에 익숙해지면, 단순히 타입을 '선언'하는 것을 넘어 타입을 '계산'하고 '조립'하는 단계로 나아갈 수 있습니다. 이것이 바로 코드 베이스가 커져도 유지보수성을 잃지 않는 시니어 개발자의 비결입니다.