1. 개요

타입을 쓰다 보면 이미 있는 타입에서 필드 몇 개만 빼거나, 전부 선택적으로 만들거나, 특정 필드만 골라 쓰고 싶은 순간이 자주 온다. 그때마다 새 인터페이스를 처음부터 다시 쓰면 원본 타입이 바뀔 때 둘 다 고쳐야 하는 문제가 생긴다. TypeScript 는 기존 타입을 변형해서 새 타입을 만드는 유틸리티 타입을 기본으로 제공한다. 이번 편은 그중 실무에서 가장 자주 쓰는 넷, Partial, Pick, Omit, Record 를 다룬다.


2. 핵심 내용

2-1. 기준이 될 타입

interface User {
  id: number
  name: string
  email: string
  role: "admin" | "member"
}

2-2. Partial<T>: 전부 선택적으로

수정 API 는 보통 바뀐 필드만 받는다. User 전체를 다시 요구하면 이름 하나만 바꾸고 싶어도 나머지 필드를 다 채워야 한다.

function updateUser(id: number, changes: Partial<User>) {
  // changes 는 { id?, name?, email?, role? } 형태
}
 
updateUser(1, { name: "새 이름" }) // OK, 나머지 필드 생략 가능

Partial<User> 는 User 의 모든 필드에 ? 를 붙인 것과 같다. React 컴포넌트의 defaultProps 나 설정 객체의 override 패턴에도 자주 쓴다.

2-3. Pick<T, K>: 필요한 필드만 골라내기

목록 화면에서는 User 전체가 아니라 id 와 name 만 필요할 때가 많다.

type UserSummary = Pick<User, "id" | "name">
// { id: number; name: string }
 
function renderList(users: UserSummary[]) {
  // email, role 은 아예 접근 불가 — 실수로 쓰면 컴파일 에러
}

User 에 필드가 추가돼도 UserSummary 는 명시한 두 필드만 유지한다. 응답 DTO 를 축소해서 프론트에 넘길 타입을 정의할 때 유용하다.

2-4. Omit<T, K>: 특정 필드만 빼기

회원가입 폼은 User 와 거의 같은데 서버가 채워줄 id 만 없다.

type SignupForm = Omit<User, "id">
// { name: string; email: string; role: "admin" | "member" }

Pick 과 Omit 은 반대 방향에서 같은 문제를 푼다. 남길 필드가 적으면 Pick, 뺄 필드가 적으면 Omit 이 코드를 더 짧게 만든다. 비밀번호처럼 응답에는 없어야 하는 필드를 걸러낼 때도 Omit<DbUser, "passwordHash"> 식으로 쓴다.

2-5. Record<K, V>: 키-값 맵의 형태를 고정하기

특정 키 집합에 대해 값을 하나씩 대응시키는 객체를 만들 때 Record 를 쓴다.

type Role = "admin" | "member" | "guest"
 
const roleLabel: Record<Role, string> = {
  admin: "관리자",
  member: "회원",
  guest: "게스트",
}

여기서 중요한 건 Record<Role, string> 이 세 키를 전부 요구한다는 점이다. 하나라도 빠뜨리면 컴파일 에러가 난다.

const incomplete: Record<Role, string> = {
  admin: "관리자",
  member: "회원",
  // guest 누락
}
// Error: Property 'guest' is missing

Role 에 "banned" 를 추가하면 roleLabel 을 안 고쳤을 때 바로 컴파일 에러로 알려준다. 평범한 { [key: string]: string } 인덱스 시그니처로 썼다면 이 검사가 없어서, 새 role 을 추가하고 라벨 매핑을 빠뜨려도 조용히 undefined 가 나왔을 상황이다.

2-6. 조합해서 쓰기

유틸리티 타입은 중첩해서 쓸 수 있다. “수정 가능한 필드만 골라서, 그것도 전부 선택적으로” 를 한 줄로 표현할 수 있다.

type EditableFields = Partial<Pick<User, "name" | "email">>
// { name?: string; email?: string }
 
function editProfile(changes: EditableFields) {}
 
editProfile({ name: "새 이름" }) // OK
editProfile({ role: "admin" }) // Error: role 은 EditableFields 에 없음

role 은 Pick 단계에서 이미 제외됐으니 Partial 을 씌워도 나타나지 않는다. 권한 필드는 사용자가 직접 못 바꾸게 타입 레벨에서 막아버린 셈이다.

2-7. 언제 새 인터페이스를 따로 쓰나

모든 걸 유틸리티 타입으로 파생시키는 게 항상 정답은 아니다. 원본 타입과 의미적으로 아예 다른 개념이라면(예: User 와 AuditLog 가 우연히 필드 몇 개를 공유하는 경우) 파생시키지 말고 따로 선언하는 게 낫다. 유틸리티 타입은 “같은 개념의 부분집합·변형” 을 표현할 때 쓰는 도구지, 우연히 모양이 비슷한 무관한 타입을 엮는 도구가 아니다.


3. 마무리

요약

  • Partial<T>: 모든 필드를 선택적으로. 부분 수정 API 에 적합.
  • Pick<T, K> / Omit<T, K>: 필요한 필드만 남기거나(Pick) 특정 필드만 뺀다(Omit). 남길 게 적으면 Pick, 뺄 게 적으면 Omit.
  • Record<K, V>: 키 집합 전체에 값이 대응돼야 하는 맵을 표현한다. 키가 늘어나면 매핑 누락을 컴파일 타임에 잡아준다.
  • 유틸리티 타입은 중첩해서 조합할 수 있지만, 원본과 의미가 다른 개념이면 억지로 파생시키지 않고 따로 선언한다.

다음은 5편이다. interface 와 type 의 실질적 차이를 다룬다.