유틸리티 타입
Partial·Required·Readonly·Pick·Omit과 유니온·함수 변환 유틸리티로 기존 타입을 목적에 맞게 재구성합니다.
타입스크립트는 복잡한 타입 변환/조합을 쉽게 수행하도록 다양한 유틸리티 타입(Utility Types)을 내장하고 있습니다.
이 유틸리티 타입 대부분은
앞서 배운 조건부 타입과 매핑된 타입 개념(keyof, typeof, infer 등)을
기반으로 구현되어 있습니다.
유틸리티 타입은 기존 타입을 재활용하여 새로운 타입을 생성하는 데 탁월하며, 타입 정의의 반복을 줄이고 코드의 가독성 및 유지보수성을 크게 향상시킵니다.
이 절에서는 가장 자주 사용되는 주요 유틸리티 타입들을 살펴보겠습니다.
객체 타입 변환 유틸리티
TYPE / RUNTIME / CONTRACT
무엇을 바꾸려는지에 따라 유틸리티 타입을 고른다
속성의 필수 여부, 키, 유니온 멤버, 함수 시그니처, 비동기 결과를 각각 다른 연산으로 계산합니다.
속성 규칙
Partial · Required · Readonly
일부 속성만 허용하거나, 모두 요구하거나, 대입을 제한합니다. 본문의 ImmutablePoint는 x:10, y:20인 값을 그대로 사용합니다.
키 선택
Pick · Omit
TodoPreview는 Todo에서 id, title, completed를 고릅니다. TodoWithoutDescription은 description만 제외하고 id, title, completed, createdAt을 남깁니다.
키별 값
Record
Page의 home, about, contact마다 PageInfo를 요구합니다. 세 키 중 하나를 빠뜨리면 이 유한 Record의 계약에 맞지 않습니다.
유니온 멤버
Exclude · Extract · NonNullable
색에서 black과 white를 제외하고, MixedValue에서 함수 타입을 추출하고, string | null | undefined에서 string을 남깁니다.
함수 시그니처
Parameters · ReturnType
MyFunction의 인자는 [name: string, age: number]입니다. 비동기 함수의 ReturnType은 Promise<number>이며 기다린 값의 타입이 아닙니다.
THIS / ASYNC
ThisParameterType · OmitThisParameter · Awaited
toHex의 this는 Number입니다. OmitThisParameter는 타입의 this 매개변수를 제거하며 bind를 실행하지 않습니다. Awaited는 중첩 Promise의 number를 얻습니다.
Partial Required Readonly는 속성 규칙, Pick Omit은 키 선택, Record는 키별 값, Exclude Extract NonNullable은 유니온, Parameters ReturnType은 함수, ThisParameterType OmitThisParameter Awaited는 this와 비동기 타입을 다룬다.
객체 타입의 속성을 수정하거나 특정 속성만 선택/제외할 때 사용하는 유틸리티 타입입니다.
Partial<T>: T의 모든 속성을 선택적(Optional)으로 만듭니다.
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
/*
// PartialUser의 실제 타입:
type PartialUser = {
id?: number;
name?: string;
email?: string;
};
*/
const userUpdate: PartialUser = {
name: "새로운 이름",
email: "[email protected]"
}; // id는 필수가 아님폼 데이터 업데이트 등, 객체의 일부만 필요한 경우에 유용합니다.
Required<T>: T의 모든 속성을 필수(Required)로 만듭니다.
Partial의 반대입니다.
interface ProductConfig {
name?: string;
price?: number;
inStock?: boolean;
}
type FullProductConfig = Required<ProductConfig>;
/*
// FullProductConfig의 실제 타입:
type FullProductConfig = {
name: string;
price: number;
inStock: boolean;
};
*/
// const config1: FullProductConfig = { name: "Test" }; // Error: 'price' 및 'inStock' 속성이 누락됨
const config2: FullProductConfig = { name: "Monitor", price: 300000, inStock: true };Readonly<T>: T의 모든 속성을 읽기 전용(readonly)으로 만듭니다.
interface Point {
x: number;
y: number;
}
type ImmutablePoint = Readonly<Point>;
/*
// ImmutablePoint의 실제 타입:
type ImmutablePoint = {
readonly x: number;
readonly y: number;
};
*/
const p: ImmutablePoint = { x: 10, y: 20 };
// p.x = 15; // Error: Cannot assign to 'x' because it is a read-only property.해당 타입 경로에서 속성 재할당을 제한합니다. 깊은 불변성이나 런타임 동결을 자동으로 만들지는 않습니다.
Pick<T, K>: T에서 K 타입에 해당하는 속성들만 선택하여 새로운 타입을 만듭니다.
interface Todo {
id: number;
title: string;
description: string;
completed: boolean;
createdAt: Date;
}
type TodoPreview = Pick<Todo, 'id' | 'title' | 'completed'>;
/*
// TodoPreview의 실제 타입:
type TodoPreview = {
id: number;
title: string;
completed: boolean;
};
*/
const todoItem: TodoPreview = {
id: 1,
title: "타입스크립트 학습",
completed: false
};특정 API 응답에서 필요한 데이터만 추출하거나, 특정 뷰에 필요한 데이터 모델을 정의할 때 유용합니다.
Omit<T, K>: T에서 K 타입에 해당하는 속성들을 제외하여 새로운 타입을 만듭니다.
type TodoWithoutDescription = Omit<Todo, 'description'>;
/*
// TodoWithoutDescription의 실제 타입:
type TodoWithoutDescription = {
id: number;
title: string;
completed: boolean;
createdAt: Date;
};
*/
const todoDisplay: TodoWithoutDescription = {
id: 2,
title: "회의 준비",
completed: true,
createdAt: new Date()
};Pick과 상호 보완적으로 사용되며, 특정 속성을 제외한 나머지가 필요할 때 유용합니다.
Record<K, T>: K 타입의 속성 이름을 가지며, 모든 속성 값이 T 타입인 객체 타입을 만듭니다.
딕셔너리 또는 맵 타입을 정의할 때 사용됩니다.
type PageInfo = {
title: string;
url: string;
};
type Page = "home" | "about" | "contact";
type SitePages = Record<Page, PageInfo>;
/*
// SitePages의 실제 타입:
type SitePages = {
home: { title: string; url: string; };
about: { title: string; url: string; };
contact: { title: string; url: string; };
};
*/
const site: SitePages = {
home: { title: "메인 페이지", url: "/" },
about: { title: "회사 소개", url: "/about" },
contact: { title: "연락처", url: "/contact" }
};동적으로 키가 생성되거나, 특정 키 집합에 대해 일관된 값 타입을 강제할 때 강력합니다.
유니온 및 함수 타입 변환 유틸리티
유니온 타입이나 함수 타입을 다룰 때 유용한 유틸리티입니다.
Exclude<T, U>: T에서 U에 할당 가능한 타입을 제외합니다. (5장 2절 "조건부 타입"에서 설명)
type MyColors = "red" | "green" | "blue" | "black" | "white";
type PrimaryColors = Exclude<MyColors, "black" | "white">; // type PrimaryColors = "red" | "green" | "blue"Extract<T, U>: T에서 U에 할당 가능한 타입만 추출합니다. (5장 2절 "조건부 타입"에서 설명)
type MixedValue = string | number | boolean | (() => void);
type OnlyFunctions = Extract<MixedValue, Function>; // type OnlyFunctions = () => voidNonNullable<T>: T에서 null과 undefined를 제외합니다. (5장 2절 "조건부 타입"에서 설명)
type NullableString = string | null | undefined;
type NonNullString = NonNullable<NullableString>; // type NonNullString = stringParameters<T>: 함수 타입 T의 매개변수 타입을 튜플로 추출합니다.
type MyFunction = (name: string, age: number) => string;
type Params = Parameters<MyFunction>; // type Params = [name: string, age: number]
type NoParams = Parameters<() => void>; // type NoParams = []ReturnType<T>: 함수 타입 T의 반환 타입을 추출합니다.
type MyFunctionReturn = ReturnType<MyFunction>; // type MyFunctionReturn = string
type AsyncReturn = ReturnType<() => Promise<number>>; // type AsyncReturn = Promise<number>내장 유틸리티 타입은 이름은 많지만, 내부 원리는 매핑된 타입, 조건부 타입, infer 추론으로 묶어 볼 수 있습니다.
고급 유틸리티 타입 (일부)
타입스크립트에는 이 외에도 특정 상황에서 유용한 고급 유틸리티 타입들이 있습니다.
ThisParameterType<T>: 함수 타입 T의 this 매개변수 타입을 추출합니다.
function toHex(this: Number, decimal: number): string {
return this.toString(16) + decimal.toString(16);
}
type ThisType = ThisParameterType<typeof toHex>; // type ThisType = NumberOmitThisParameter<T>: 함수 타입 T에서 this 매개변수를 제거한 새로운 함수 타입을 만듭니다.
type NoThisToHex = OmitThisParameter<typeof toHex>; // type NoThisToHex = (decimal: number) => string
// const simplifiedHex: NoThisToHex = toHex; // 타입만 바꿔 대입해도 런타임 receiver가 고정되지는 않습니다.
// 타입만 제거할 뿐 런타임 동작이 변하는 것은 아닙니다.Awaited<T> (TypeScript 4.5+): Promise와 같은 awaitable 타입의 최종 resolved 값을 재귀적으로 추출합니다.
type PromiseOfNumbers = Promise<Promise<number>>;
type Result = Awaited<PromiseOfNumbers>; // type Result = number
type ValueOrPromise = string | Promise<number>;
type AwaitedValueOrPromise = Awaited<ValueOrPromise>; // type AwaitedValueOrPromise = string | number비동기 함수의 반환 타입을 정확히 추론할 때 매우 유용합니다.
다음 그림은 유틸리티의 정적 타입 변환과 실제 공개 값 생성을 구분합니다. 앞의 각 유틸리티 예제는 타입 계약을 설명하고, 아래의 별도 확장 실습은 허용한 속성만 새 객체로 만드는 함수를 직접 작성합니다. 외부 입력 validator가 자동으로 생기는 것은 아닙니다.
유틸리티 타입과 데이터 런타임 경계 — 상태 추적
실제 별도 실습에서 같은 원본을 typedView로 읽을 때 세 키가 남고 toPublicProfile 호출은 두 키의 새 객체를 만든다. 객체 정체성과 실제 키/별칭 변화로 정적 Omit이 삭제를 하지 않음을 보여 줄 가치가 있다.
TYPE / RUNTIME / CONTRACT
유틸리티 타입을 적용해도 값은 저절로 바뀌지 않는다
Partial, Required, Readonly, Pick, Omit은 새로운 타입을 계산합니다. 초기값 생성, 객체 복사, 속성 제거는 실행 코드의 역할입니다.
PARTIAL / REQUIRED
허용하거나 요구하는 속성
userUpdate는 name과 email을 담고 id는 생략한 PartialUser 객체입니다. FullProductConfig는 name, price, inStock을 모두 요구하지만 값을 채워주지는 않습니다.
READONLY
대입을 제한하는 타입
ImmutablePoint의 x, y에 대한 직접 대입이 제한됩니다. Object.freeze를 실행하거나 중첩 객체까지 자동으로 동결하지 않습니다.
PICK / OMIT
타입에 보이는 키를 바꾼다
TodoPreview는 id, title, completed를 고르고, TodoWithoutDescription은 description만 제외해 createdAt은 유지합니다. 기존 객체를 이 타입으로 참조해도 실제 다른 속성이 삭제되지는 않습니다.
RUNTIME · 설계 보충
공개할 값을 직접 만든다
외부 값을 받으면 실제 검증 코드를 작성합니다. 공개 응답에서는 허용한 속성만 새 객체로 복사해야 실제 결과의 키도 바뀝니다.
본문은 각 타입을 사용한 객체 리터럴 예제입니다. 검증기나 응답 변환기는 유틸리티 타입이 자동으로 만드는 구현이 아닙니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 동일 StoredProfile에서 서로 독립적인 두 경로는 각각 노드 3개·간선 2개로 구성된다. 타입 대입→typedView 동일 객체는 실제 id,name,email 세 키를 유지한다. 함수 호출→새 PublicProfile 객체는 id,name 두 키만 복사한다. 두 결과 간 전환 간선은 없으며 외부 입력 검증이나 깊은 복사가 아니다. |
| 전체 관계·조건 설명 | 동일 StoredProfile에서 서로 독립적인 두 경로는 각각 노드 3개·간선 2개로 구성된다. 타입 대입→typedView 동일 객체는 실제 id,name,email 세 키를 유지한다. 함수 호출→새 PublicProfile 객체는 id,name 두 키만 복사한다. 두 결과 간 전환 간선은 없으며 외부 입력 검증이나 깊은 복사가 아니다. 모바일에서도 노드와 연결 방향·독립 경로가 같다. |
| 원도표 설명 | userUpdate는 name과 email을 담고 id는 생략한 PartialUser 객체입니다. FullProductConfig는 name, price, inStock을 모두 요구하지만 값을 채워 주지는 않습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 저장 객체 | 원본: 키 3개 / identity: 원본 객체 | StoredProfile은 id·name·email의 세 키를 갖는다 | 정적 PublicProfile 타입을 선언해도 원본은 바뀌지 않는다 |
| typedView | typedView: 원본과 같은 객체 / 실제 키: 3개 | 같은 객체를 PublicProfile 타입으로 참조한다 | 타입에서 email을 보이지 않게 해도 runtime 키는 삭제되지 않는다 |
| 공개 값 생성 | public: 새 객체 키 2개 / 원본: 키 3개 유지 | toPublicProfile이 id와 name을 복사한 새 객체를 만든다 | allowlist 복사는 입력 검증이나 깊은 복사가 아니다 |
유틸리티 타입과 데이터 런타임 경계 — 상태 추적
실제 별도 실습에서 같은 원본을 typedView로 읽을 때 세 키가 남고 toPublicProfile 호출은 두 키의 새 객체를 만든다. 객체 정체성과 실제 키/별칭 변화로 정적 Omit이 삭제를 하지 않음을 보여 줄 가치가 있다.
TYPE / RUNTIME / CONTRACT
유틸리티 타입을 적용해도 값은 저절로 바뀌지 않는다
Partial, Required, Readonly, Pick, Omit은 새로운 타입을 계산합니다. 초기값 생성, 객체 복사, 속성 제거는 실행 코드의 역할입니다.
PARTIAL / REQUIRED
허용하거나 요구하는 속성
userUpdate는 name과 email을 담고 id는 생략한 PartialUser 객체입니다. FullProductConfig는 name, price, inStock을 모두 요구하지만 값을 채워주지는 않습니다.
READONLY
대입을 제한하는 타입
ImmutablePoint의 x, y에 대한 직접 대입이 제한됩니다. Object.freeze를 실행하거나 중첩 객체까지 자동으로 동결하지 않습니다.
PICK / OMIT
타입에 보이는 키를 바꾼다
TodoPreview는 id, title, completed를 고르고, TodoWithoutDescription은 description만 제외해 createdAt은 유지합니다. 기존 객체를 이 타입으로 참조해도 실제 다른 속성이 삭제되지는 않습니다.
RUNTIME · 설계 보충
공개할 값을 직접 만든다
외부 값을 받으면 실제 검증 코드를 작성합니다. 공개 응답에서는 허용한 속성만 새 객체로 복사해야 실제 결과의 키도 바뀝니다.
본문은 각 타입을 사용한 객체 리터럴 예제입니다. 검증기나 응답 변환기는 유틸리티 타입이 자동으로 만드는 구현이 아닙니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 동일 StoredProfile에서 서로 독립적인 두 경로는 각각 노드 3개·간선 2개로 구성된다. 타입 대입→typedView 동일 객체는 실제 id,name,email 세 키를 유지한다. 함수 호출→새 PublicProfile 객체는 id,name 두 키만 복사한다. 두 결과 간 전환 간선은 없으며 외부 입력 검증이나 깊은 복사가 아니다. |
| 전체 관계·조건 설명 | 동일 StoredProfile에서 서로 독립적인 두 경로는 각각 노드 3개·간선 2개로 구성된다. 타입 대입→typedView 동일 객체는 실제 id,name,email 세 키를 유지한다. 함수 호출→새 PublicProfile 객체는 id,name 두 키만 복사한다. 두 결과 간 전환 간선은 없으며 외부 입력 검증이나 깊은 복사가 아니다. 모바일에서도 노드와 연결 방향·독립 경로가 같다. |
| 원도표 설명 | userUpdate는 name과 email을 담고 id는 생략한 PartialUser 객체입니다. FullProductConfig는 name, price, inStock을 모두 요구하지만 값을 채워 주지는 않습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 저장 객체 | 원본: 키 3개 / identity: 원본 객체 | StoredProfile은 id·name·email의 세 키를 갖는다 | 정적 PublicProfile 타입을 선언해도 원본은 바뀌지 않는다 |
| typedView | typedView: 원본과 같은 객체 / 실제 키: 3개 | 같은 객체를 PublicProfile 타입으로 참조한다 | 타입에서 email을 보이지 않게 해도 runtime 키는 삭제되지 않는다 |
| 공개 값 생성 | public: 새 객체 키 2개 / 원본: 키 3개 유지 | toPublicProfile이 id와 name을 복사한 새 객체를 만든다 | allowlist 복사는 입력 검증이나 깊은 복사가 아니다 |
유틸리티 타입은 원본 타입을 직접 바꾸지 않고 목적별 계약을 파생시키는 도구입니다.
유틸리티 타입은 기존 타입을 선택, 제외, 부분화, 읽기 전용화하는 재사용 가능한 타입 변환 도구입니다.
내장 유틸리티 타입을 사용하면 반복적인 타입 선언을 줄이고, 객체 구조 변화에 맞춰 타입 계약을 더 일관되게 유지할 수 있습니다.
타입으로 선택한 속성과 실제 응답의 키 비교하기
앞의 Pick 예제는 필요한 키를 가진 새 객체 리터럴을 직접 작성했습니다. 이미 만들어진 큰 객체를 작은 타입으로 참조하면 다른 결과가 생깁니다. 아래는 원래 User와 이름이 겹치지 않는 별도 공개 프로필 실습입니다.
interface StoredProfile {
id: number;
name: string;
email: string;
}
type PublicProfile = Pick<StoredProfile, "id" | "name">;
const storedProfile: StoredProfile = {
id: 7,
name: "지수",
email: "[email protected]"
};
const typedView: PublicProfile = storedProfile;
function toPublicProfile(profile: StoredProfile): PublicProfile {
return { id: profile.id, name: profile.name };
}
const publicProfile = toPublicProfile(storedProfile);
console.log(Object.keys(typedView).join(",")); // id,name,email
console.log(Object.keys(publicProfile).join(",")); // id,name
console.log(typedView === storedProfile); // true
console.log(publicProfile === storedProfile); // falsetypedView의 정적 타입에서는 email을 사용할 수 없지만 실제 값은 같은 원본 객체입니다. 타입 표기는 객체를 복사하지 않아 실행 결과에 세 키가 남습니다. toPublicProfile은 허용한 두 속성만 읽어 새 객체를 만들므로 실제 결과에서도 email이 빠집니다. 여기서는 세 속성의 값이 원시 값인 예시이며, 이 함수가 모든 외부 입력을 검증하거나 중첩 객체를 깊게 복사한다는 뜻은 아닙니다.
공개 키를 바꾸는 독립 연습
공개 응답을 "id" | "email" 두 속성으로 바꾸려 합니다. 타입만 바꾸는 것과 생성 함수를 바꾸는 것 중 무엇이 필요할까요? 원래 name을 외부 결과에서 빼려면 타입과 함수의 반환 객체를 각각 어떻게 고쳐야 하는지 적고, 생성 결과의 키와 원본 객체의 키를 예상해 보세요.
계약과 실제 객체를 함께 바꾸기
Pick<StoredProfile, "id" | "email">로 계약을 바꾸고, 반환 객체도 { id: profile.id, email: profile.email }로 바꿉니다. 함수의 실행 코드를 그대로 두면 반환 객체에 선택하지 않은 name이 남고 필요한 email은 없어 새 계약과 맞지 않습니다. 고친 생성 결과의 키는 id,email이고 원본 객체의 키는 여전히 id,name,email입니다. 원본에서 속성을 삭제한 것이 아니라 새 공개 객체를 만든 것입니다.
Omit<StoredProfile, "name">도 이 예시에서는 같은 두 속성 계약을 만듭니다. 다만 원본에 새 속성이 추가되면 Omit에는 남을 수 있으므로, 공개할 속성을 열거하려는 목적에는 허용 키를 직접 고르는 Pick이 더 분명합니다.
Pick과 Omit은 공식 유틸리티 타입 설명처럼 타입의 속성 집합을 계산합니다. 실제 공개 응답을 만들 때는 타입 결과와 실행 결과를 둘 다 확인합니다.