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 missingRole 에 "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 의 실질적 차이를 다룬다.