유틸리티 타입은 TypeScript에 내장된 제네릭 타입으로, 한 타입을 다른 타입으로 바꿉니다. 모든 필드가 선택적인 User 타입을 하나 더 쓰는 대신 Partial<User>라고 쓰고, 필드 세 개를 복사하는 대신 Pick<User, "id" | "name">이라고 씁니다. 전역 타입이므로 import가 필요 없습니다.
User가 바뀌면 파생된 네 타입도 모두 따라 바뀝니다. TypeScript 표준 라이브러리(lib.es5.d.ts)에는 유틸리티 타입이 22개 선언되어 있습니다. 아래 섹션에서는 다루는 타입의 종류별로 모두 정리하고, 자세한 페이지가 있으면 링크를 달았습니다.
객체 타입: Partial, Required, Readonly, Pick, Omit, Record
| 유틸리티 타입 | 하는 일 | 예시 |
|---|---|---|
Partial<T> | 모든 속성을 선택적으로 만든다 | 업데이트 페이로드용 Partial<User> |
Required<T> | 모든 속성을 필수로 만든다(? 제거) | 기본값을 적용한 뒤의 Required<Config> |
Readonly<T> | 모든 속성을 readonly로 만든다 | Readonly<State> |
Pick<T, K> | 키 K만 남긴다 | Pick<User, "id" | "name"> |
Omit<T, K> | 키 K를 제거한다 | Omit<User, "password"> |
Record<K, V> | 키가 K, 값이 V인 객체 타입 | Record<"en" | "de", string> |
Required<T>는 Partial<T>의 반대입니다. Partial 페이지에서 둘 다 다루며, 이런 스프레드가 통과시키는 명시적 undefined 문제도 설명합니다.
유니언 타입: Exclude, Extract, NonNullable
| 유틸리티 타입 | 하는 일 | 예시 |
|---|---|---|
Exclude<U, M> | M에 대입 가능한 유니언 멤버를 제거한다 | Exclude<"a" | "b" | "c", "a">는 "b" | "c" |
Extract<U, M> | M에 대입 가능한 유니언 멤버만 남긴다 | Extract<string | number, number>는 number |
NonNullable<T> | null과 undefined를 제거한다 | NonNullable<string | null>은 string |
이 세 가지는 객체가 아니라 유니언에 동작합니다. 객체 타입과 그 키 목록을 받는 Pick, Omit과의 핵심 차이가 바로 이것입니다.
함수와 클래스 타입: Parameters, ReturnType 등
| 유틸리티 타입 | 하는 일 | 예시 |
|---|---|---|
ReturnType<F> | 함수 타입의 반환 타입 | ReturnType<typeof createStore> |
Parameters<F> | 매개변수 타입을 튜플로 | Parameters<typeof fetchPage>[0] |
ConstructorParameters<C> | 클래스 생성자의 매개변수를 튜플로 | ConstructorParameters<typeof Point> |
InstanceType<C> | 생성자가 만드는 인스턴스 타입 | InstanceType<typeof Point> |
ThisParameterType<F> | 함수의 this 매개변수 타입 | ThisParameterType<typeof greet> |
OmitThisParameter<F> | this 매개변수를 뺀 함수 타입 | greet.bind(obj)의 타입 |
ThisType<T> | 객체 리터럴 메서드 안의 this 타입을 정한다 | 빌더 API에서 noImplicitThis와 함께 사용 |
NoInfer<T> | 이 위치에서 타입 매개변수가 추론되지 않게 막는다 | fallback: NoInfer<C> |
이 유틸리티들은 타입을 받는데 createOrder는 값이므로 typeof createOrder가 필요합니다. 클래스도 마찬가지입니다. typeof Point는 생성자 타입이고, 타입 자리에 쓴 Point는 이미 인스턴스 타입을 뜻합니다.
NoInfer는 제네릭이 타입을 어디서 가져올지 제어합니다.
NoInfer가 없으면 TypeScript는 두 인수 모두에서 C를 추론해 "red" | "green" | "blue"로 넓히므로, fallback의 오타가 그대로 통과합니다.
문자열 타입: Uppercase, Lowercase, Capitalize, Uncapitalize
| 유틸리티 타입 | 하는 일 | 예시 |
|---|---|---|
Uppercase<S> | 문자열 리터럴 타입을 대문자로 | Uppercase<"get">은 "GET" |
Lowercase<S> | 소문자로 | Lowercase<"GET">은 "get" |
Capitalize<S> | 첫 글자를 대문자로 | Capitalize<"name">은 "Name" |
Uncapitalize<S> | 첫 글자를 소문자로 | Uncapitalize<"Name">은 "name" |
이 네 가지는 TypeScript로 작성된 것이 아니라 컴파일러에 내장되어 있으며, 이벤트 핸들러 이름을 만드는 `on${Capitalize<E>}`처럼 템플릿 리터럴 타입 안에서 가장 유용합니다.
Promise: Awaited
| 유틸리티 타입 | 하는 일 | 예시 |
|---|---|---|
Awaited<T> | 중첩된 Promise를 풀어 await로 얻는 타입 | Awaited<Promise<Promise<number>>>는 number |
Awaited<ReturnType<typeof fn>>은 async 함수의 결과 타입을 따로 선언하지 않고 이름 붙이는 표준 방법입니다.
유틸리티 타입 조합하기
유틸리티 타입은 중첩할 수 있습니다. 자주 나오는 조합 몇 가지는 외워 둘 만합니다.
중첩된 유틸리티 타입은 안쪽부터 읽습니다. Readonly<Pick<Post, "id" | "title">>는 먼저 속성 두 개를 남기고, 그다음 읽기 전용으로 만듭니다. 같은 부품으로 일부 키만 선택적으로 만드는 PartialBy 헬퍼도 만들 수 있으며, Partial 페이지에 구현이 있습니다.
유틸리티 타입은 런타임에 아무것도 하지 않는다
모든 유틸리티 타입은 컴파일할 때 지워집니다. Omit<User, "password"> 타입의 값도 원래 객체에 password가 있었다면 런타임에 그대로 가지고 있을 수 있습니다.
타입은 코드가 무엇을 읽을 수 있는지만 제한합니다. 데이터에서 필드를 제거하려면 마지막 줄처럼 구조 분해로 떼어 내고, 런타임 변경을 막으려면 Readonly가 아니라 Object.freeze를 쓰세요. 내장 타입은 한 줄짜리 매핑된 타입과 조건부 타입이므로, 같은 도구로 직접 만들 수도 있습니다.
자주 묻는 질문
TypeScript에서 유틸리티 타입이란 무엇인가요?
TypeScript에 기본으로 들어 있으며 다른 타입을 변환하는 제네릭 타입입니다. Partial<T>는 모든 속성을 선택적으로 만들고, Pick<T, K>는 일부 속성만 남기고, ReturnType<F>는 함수의 반환 타입을 가져옵니다. 표준 라이브러리에 선언되어 있으므로 아무것도 import하지 않고 씁니다.
유틸리티 타입을 import해야 하나요?
아니요. Partial, Omit, Record, ReturnType 등은 TypeScript 내장 라이브러리 파일에 있는 전역 타입입니다. 어디서든 Partial<User>라고 쓰면 되고, import도 npm 패키지도 필요 없습니다.
TypeScript에 내장된 유틸리티 타입은 무엇인가요?
모두 lib.es5.d.ts에 선언된 22개입니다: Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, Parameters, ConstructorParameters, ReturnType, InstanceType, ThisParameterType, OmitThisParameter, ThisType, NoInfer, Awaited, Uppercase, Lowercase, Capitalize, Uncapitalize.
유틸리티 타입이 런타임에 객체를 바꾸나요?
아니요. 타입만 설명하며 JavaScript 출력에서는 지워집니다. Omit<User, "password">는 password 속성을 삭제하지 않고, Readonly<T>는 아무것도 동결하지 않습니다. 실제 객체를 바꾸려면 구조 분해 나머지 패턴, Object.freeze 같은 코드를 직접 써야 합니다.
유틸리티 타입을 직접 만들 수 있나요?
네. 내장 유틸리티 타입도 평범한 TypeScript이며, 대부분 lib.es5.d.ts에 있는 한 줄짜리 매핑된 타입이나 조건부 타입입니다. type Nullable<T> = { [K in keyof T]: T[K] | null }도 같은 방식으로 쓴 사용자 정의 유틸리티 타입입니다.