typescript · high priority
TypeScript Utility Types — Pick · Omit · Partial · Record + 직접 구현
내장 유틸리티가 어떻게 만들어졌는지 알면, 직접 만들어 쓸 수 있다
학습 개요
탄생 배경
쉬운 설명
복잡한 개념을 실생활 비유로 설명합니다.
“레고 부품 카탈로그”
내장 utility 들은 미리 만들어진 레고 키트입니다. Pick 은 "필요한 부품만 골라 담는 상자", Omit 은 "이 부품만 빼고 나머지", Record 는 "이름표마다 같은 부품을 담는 정리함", Partial 은 "모든 부품을 옵션으로 만든 키트", Required 는 그 반대. 그런데 모든 키트는 결국 같은 *기초 블록*(keyof, mapped, conditional, infer) 으로 조립됩니다. 기초 블록을 알면 카탈로그에 없는 키트도 직접 만들 수 있습니다.
핵심 개념
keyof T
T 의 모든 공개 키들을 *유니언 타입* 으로 돌려준다. keyof { a: 1; b: 2 } 는 "a" | "b".
K in keyof T (mapped type)
{ [K in keyof T]: ... } 형태로 *모든 키를 순회* 하며 새 타입을 만든다. utility 의 절반 이상이 이 패턴.
T[K] (indexed access)
T 에서 키 K 의 값 타입을 끄집어낸다. mapped type 안에서 매번 등장.
T extends U ? X : Y (conditional)
T 가 U 의 부분타입이면 X, 아니면 Y. 분배 법칙(distributive) 이 적용되는 경우가 있어 유니언과의 상호작용을 알아야 한다.
+? / -? · +readonly / -readonly
mapped type 에서 옵셔널/readonly 수정자를 *붙이거나 제거* 한다. Required 는 -?, Readonly 는 readonly.
1interface User {2 id: string;3 name: string;4 age: number;5}67type UserKeys = keyof User; // "id" | "name" | "age"8type UserName = User['name']; // string9type UserStringKeys = { // 값이 string 인 키만10 [K in keyof User]: User[K] extends string ? K : never11}[keyof User];12// → "id" | "name"
실무 적용
어떤 상황에서 사용하는가
백엔드가 `User` 도메인 타입을 한 번 정의하고, 클라이언트는 (1) 가입 폼 입력 중간 상태(부분), (2) 응답 DTO(비밀번호 제외), (3) 캐시 사전(userId → User), (4) Profile 일부만 골라 보여주는 카드 — 4 가지 형태가 모두 필요하다.
어떻게 적용하는가
(1) 폼 입력은 `DeepPartial<User>` 로 표현. (2) 응답 DTO 는 `Omit<User, "passwordHash" | "internalNotes">`. (3) 캐시는 `Record<User["id"], User>` 또는 `Map<User["id"], User>` (런타임 자료구조 일치). (4) 카드는 `Pick<User, "id" | "nickname" | "avatarUrl">`. 한 곳의 `User` 변경이 네 군데로 자동 전파되어 *수동 동기화 부담* 이 사라진다.
흔한 실수와 안티패턴
- `Partial` 을 깊은 구조에 그대로 써 버그를 만드는 것 — 한 단계만 옵셔널이라는 사실 숙지.
- `Record<string, V>` 를 무비판하게 사용 — `noUncheckedIndexedAccess` 와의 상호작용을 모르면 런타임 undefined 사고.
- `Omit` 으로 키를 빼고 나머지 키를 추가했다고 안전하다고 착각 — 키 충돌·이름 변경에는 여전히 깨짐. `satisfies` 와 함께 검증.
- utility 를 너무 많이 합성해 한 줄짜리 타입이 IDE 호버에서 폭발 — 가독성을 위해 중간 단계에 `type` alias 를 잘라 두기.
흔한 오해
"`Partial<T>` 는 모든 깊이를 옵셔널로 만든다."
교정한 단계만 옵셔널. 깊은 구조는 직접 `DeepPartial` 을 짜야 한다.
왜 중요내장 정의는 `[P in keyof T]?: T[P]` 로 *값 타입을 그대로* 둔다. 재귀가 없다.
"`Omit` 은 존재하지 않는 키를 줘도 안전하다."
교정`Omit<T, K>` 의 `K` 는 `keyof any` 까지 허용해 *오타가 컴파일 에러로 잡히지 않는다*. 의도와 다른 타입이 만들어진다.
왜 중요TC39 가 "기존 코드 호환" 을 위해 그렇게 정의했다. 엄격한 키 체크가 필요하면 `Omit<T, keyof T & K>` 같은 패턴이나 `StrictOmit` 헬퍼를 직접 만든다.
"`Record<string, V>` 와 `{ [k: string]: V }` 는 같다."
교정거의 같지만 *키가 유니언* 으로 닫혀 있을 때(예: `Record<"a" | "b", V>`) 의미가 달라진다.
왜 중요Record 는 키 집합이 닫혀 있으면 모든 키가 *반드시* 존재해야 한다. 인덱스 시그니처는 키 형태만 제약하고 존재 여부는 체크하지 않는다.
면접 질문
답변 방향 힌트
`keyof`, mapped type, `+?/-?`, `+readonly/-readonly` 만 알면 다 짜진다.
반드시 언급할 키워드
- `Pick<T, K extends keyof T> = { [P in K]: T[P] }`
- `Omit<T, K> = Pick<T, Exclude<keyof T, K>>` — Exclude 는 conditional + distributive
- `Partial<T> = { [P in keyof T]?: T[P] }`
- `Required<T> = { [P in keyof T]-?: T[P] }` — `-?` 가 결정적
- `Readonly<T> = { readonly [P in keyof T]: T[P] }`
예상 꼬리 질문
- `Omit` 의 `K` 가 `keyof T` 가 아니라 `keyof any` 인 이유는? 그게 왜 위험한가요?
- `Required` 의 `-?` 와 `& {}` 트릭의 차이는?
자기 점검
`Partial<T>` 가 깊은 구조에 안전하지 않은 이유를 한 문장으로 설명하라.
기대 키워드
자주 하는 오해
"모든 필드가 옵셔널" 이라는 한 줄 요약만 외우면 깊은 객체에서 버그가 난다. 내장은 *재귀 없이 한 단계만* 옵셔널화한다.
`Omit<T, "x">` 에서 `"x"` 가 T 의 키가 아니어도 컴파일이 통과되는 이유는?
기대 키워드
자주 하는 오해
엄격한 체크를 기대하기 쉽지만 표준 정의는 호환성 때문에 `keyof any` 까지 허용한다. 안전성이 필요하면 `StrictOmit` 같은 헬퍼를 직접 정의한다.
conditional type 의 분배 규칙(distributive) 을 막아야 하는 상황을 한 가지 들고, 어떻게 막는지 설명하라.
기대 키워드
자주 하는 오해
"분배가 항상 좋다" 가 아니다. 유니언 전체를 한 덩어리로 평가하고 싶을 때 — 예: 동일성 체크 — 는 `[T] extends [U]` 로 분배를 비활성화해야 의도대로 동작한다.
학습 자료
- Utility Types — TypeScript HandbookPick · Omit · Partial · Required · Readonly · Record · ReturnType · Awaited 등 모든 내장 유틸리티의 공식 정의와 예시.Doctypescriptlang.org
- Mapped Types & Conditional Types`keyof`, mapped, conditional, `infer` 의 의미론. 직접 utility 를 짜기 전에 한 번은 정독해야 하는 챕터.Doctypescriptlang.org
- type-fest — community-maintained extra utility typesDeepPartial, RequireAtLeastOne, SetOptional 등 표준에는 없지만 자주 쓰이는 utility 들의 검증된 구현 모음.Codesindresorhus/type-fest
- type-challenges난이도별 타입 퍼즐 모음. utility 들을 직접 구현하며 정신 모델을 단단하게 만든다.Codetype-challenges