본문 바로가기

유틸리티 타입

표준 유틸리티 타입을 바꾸려는 대상에 따라 속성 규칙, 키 선택, 유니온, 함수, 비동기 결과로 나누어 예제로 정리하고, 유틸리티 타입이 타입만 계산할 뿐 실제 객체의 속성을 고르거나 지우지 않는다는 점을 공개 프로필 예제로 확인합니다.

이 장에서 만든 조건부 타입과 매핑된 타입, infer는 표준 라이브러리의 유틸리티 타입(utility type)을 이루는 재료입니다. 자주 쓰는 변환은 이미 이름이 붙은 유틸리티로 들어 있으므로, 원리를 알고 나면 직접 만들기보다 표준 유틸리티를 골라 쓰는 일이 더 많습니다.

이 글은 표준 유틸리티를 무엇을 바꾸려는지에 따라 나누어 예제로 정리합니다. 마지막으로 유틸리티 타입이 타입만 계산할 뿐 실제 객체를 바꾸지 않는다는 점을, 공개할 속성만 고르는 예제로 확인합니다. 예제는 ts-first 프로젝트에 utility-types.ts를 만들고 include를 그 파일로 바꾸어 확인합니다.

Partial Required Readonly는 속성 규칙, Pick Omit은 키 선택, Record는 키별 값, Exclude Extract NonNullable은 유니온, Parameters ReturnType은 함수, ThisParameterType OmitThisParameter Awaited는 this와 비동기 타입을 다룬다.

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와 비동기 타입을 다룬다.

위 그림처럼 유틸리티를 고를 때는 먼저 무엇을 바꾸려는지 정합니다. 속성의 필수 여부와 읽기 전용, 남길 키, 유니온의 멤버, 함수의 매개변수와 반환값, 비동기 결과는 각각 다른 유틸리티가 맡습니다. 그림의 카드 가운데 ImmutablePoint는 본문의 fixedPoint에 해당하고, toHex의 this를 Number로 적은 부분은 본문이 this: number로 선언했으므로 원시 타입 number로 읽어야 합니다.

속성의 규칙 바꾸기

Partial<T>는 모든 속성을 선택적으로, Required<T>는 모든 속성을 필수로, Readonly<T>는 모든 속성을 읽기 전용으로 만듭니다. 앞 글들에서 직접 만든 PartialType, RequiredType, ReadonlyType과 같은 선언입니다.

interface User {
  id: number;
  name: string;
  email: string;
}

interface ProductConfig {
  name?: string;
  price?: number;
  inStock?: boolean;
}

interface Point {
  x: number;
  y: number;
}

const userUpdate: Partial<User> = { name: "새로운 이름" };
const fullConfig: Required<ProductConfig> = { name: "Monitor", price: 300000, inStock: true };
const fixedPoint: Readonly<Point> = { x: 10, y: 20 };

console.log(userUpdate.name, fullConfig.price, fixedPoint.x);
새로운 이름 300000 10

Partial<User>는 일부 속성만 바꾸는 수정 요청처럼 객체의 일부만 다룰 때 씁니다. Required<ProductConfig>의 변수에 { name: "Test" }만 넣으면 price와 inStock이 빠졌다는 진단이 나오고, Readonly<Point>의 fixedPoint.x = 15는 TS2540으로 진단됩니다. 다만 Required가 빠진 값을 채워 주지는 않고, Readonly가 객체를 얼리지도 않습니다.

남길 키 고르기

Pick<T, K>는 T에서 키 K의 속성만 남기고, Omit<T, K>는 키 K의 속성을 뺍니다. Record<K, V>는 키 K마다 값이 V인 객체 타입을 만듭니다.

interface Todo {
  id: number;
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
}

type TodoPreview = Pick<Todo, "id" | "title" | "completed">;
type TodoWithoutDescription = Omit<Todo, "description">;

type PageInfo = { title: string; url: string };
type Page = "home" | "about" | "contact";

const todoItem: TodoPreview = { id: 1, title: "타입스크립트 학습", completed: false };
const todoDisplay: TodoWithoutDescription = {
  id: 2,
  title: "회의 준비",
  completed: true,
  createdAt: new Date("2026-03-02T09:00:00Z"),
};
const site: Record<Page, PageInfo> = {
  home: { title: "메인 페이지", url: "/" },
  about: { title: "회사 소개", url: "/about" },
  contact: { title: "연락처", url: "/contact" },
};

console.log(todoItem.title, todoDisplay.createdAt.toISOString(), site.about.url);
타입스크립트 학습 2026-03-02T09:00:00.000Z /about

TodoPreview는 목록 화면처럼 세 속성만 필요한 곳에, TodoWithoutDescription은 설명만 빼고 나머지가 모두 필요한 곳에 씁니다. Record<Page, PageInfo>는 키가 정해진 유니온이므로, 앞 글에서 본 것처럼 세 페이지를 모두 적어야 하고 하나라도 빠지면 진단됩니다.

유니온 다루기

Exclude와 Extract, NonNullable은 유니온의 멤버를 걸러 냅니다. Exclude와 Extract의 원리는 5장 2편에서 본 분배 조건부 타입입니다. NonNullable은 같은 글에서 본 것처럼 T & {}로 선언되어, {}와의 인터섹션이 null과 undefined를 걸러 냅니다.

type MyColors = "red" | "green" | "blue" | "black" | "white";
type PrimaryColors = Exclude<MyColors, "black" | "white">;

type MixedValue = string | number | (() => void);
type OnlyFunctions = Extract<MixedValue, Function>;

type NonNullString = NonNullable<string | null | undefined>;
타입결과
Exclude<MyColors, "black" | "white">"red" | "green" | "blue"
Extract<MixedValue, Function>() => void
NonNullable<string | null | undefined>string

Exclude는 두 번째 인수에 할당되는 멤버를 빼고, Extract는 그런 멤버만 남깁니다. 함수 타입 () => void만 Function에 할당되므로 OnlyFunctions는 그 함수 타입 하나가 됩니다.

함수 타입 다루기

Parameters<T>는 함수 타입의 매개변수 목록을 튜플로, ReturnType<T>는 반환 타입을 꺼냅니다. this 매개변수가 있는 함수에서는 ThisParameterType<T>로 this의 타입을 꺼내거나, OmitThisParameter<T>로 this를 뺀 함수 타입을 만들 수 있습니다.

type MyFunction = (name: string, age: number) => string;

type Params = Parameters<MyFunction>;
type MyFunctionReturn = ReturnType<MyFunction>;

function toHex(this: number, suffix: number): string {
  return this.toString(16) + suffix.toString(16);
}

type ToHexThis = ThisParameterType<typeof toHex>;
type ToHexWithoutThis = OmitThisParameter<typeof toHex>;

console.log(toHex.call(255, 1));
ff1

Params는 [name: string, age: number], MyFunctionReturn은 string입니다. typeof toHex는 함수 toHex의 타입을 꺼내는 타입 연산자이고, ToHexThis는 number, ToHexWithoutThis는 (suffix: number) => string입니다. toHex.call(255, 1)은 this를 255로 정해 부르므로 "ff"와 "1"을 이은 ff1이 출력되며, OmitThisParameter는 타입에서 this를 뺄 뿐 실행 중의 this를 정해 주지는 않습니다.

비동기 결과 꺼내기

Awaited<T>는 await가 돌려줄 값의 타입을 계산합니다. Promise가 여러 겹이면 끝까지 풀고, 유니온이면 멤버마다 따로 풉니다.

async function loadCount(): Promise<number> {
  return 3;
}

type LoadCountReturn = ReturnType<typeof loadCount>;
type LoadedCount = Awaited<LoadCountReturn>;
type Nested = Awaited<Promise<Promise<number>>>;
type Mixed = Awaited<string | Promise<number>>;

LoadCountReturn은 Promise<number>이고, 여기에 Awaited를 씌운 LoadedCount는 number입니다. Nested도 두 겹을 모두 풀어 number가 되고, Mixed는 string은 그대로 두고 Promise<number>만 풀어 string | number가 됩니다. 비동기 함수가 결국 무엇을 돌려주는지 타입으로 쓸 때 Awaited<ReturnType<typeof 함수>>의 모양을 자주 씁니다.

타입은 값을 바꾸지 않는다

유틸리티 타입은 새 타입을 계산할 뿐, 실제 객체에서 속성을 고르거나 지우지 않습니다. 회원 정보에서 공개할 속성만 고르는 예제로 이 차이를 확인합니다.

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(","));
console.log(Object.keys(publicProfile).join(","));
console.log(typedView === storedProfile, publicProfile === storedProfile);
id,name,email
id,name
true false

typedView의 타입으로는 email을 쓸 수 없지만, 실제로는 원래 객체를 그대로 가리키므로 세 키가 모두 남아 있습니다. 이 값을 그대로 응답으로 보내면 email도 함께 나가게 됩니다. 반면 toPublicProfile은 허용한 두 속성만 읽어 새 객체를 만들므로, 실행 결과에서도 email이 빠집니다.

유틸리티 타입과 데이터 런타임 경계 — 상태 추적

실제 별도 실습에서 같은 원본을 typedView로 읽을 때 세 키가 남고 toPublicProfile 호출은 두 키의 새 객체를 만든다. 객체 정체성과 실제 키/별칭 변화로 정적 Omit이 삭제를 하지 않음을 보여 줄 가치가 있다.

본문의 유틸리티 타입은 정적 속성 규칙을 바꾼다. Required는 값을 채우지 않고 Readonly는 객체를 동결하지 않으며 Omit은 실제 속성을 삭제하지 않는다.

TYPE / RUNTIME / CONTRACT

유틸리티 타입을 적용해도 값은 저절로 바뀌지 않는다

Partial, Required, Readonly, Pick, Omit은 새로운 타입을 계산합니다. 초기값 생성, 객체 복사, 속성 제거는 실행 코드의 역할입니다.

타입으로 보는 경로와 실제 객체 생성 경로동일StoredProfile에서서로독립인두3노드2간선경로를비교한다. 타입대입→typedView동일객체는실제id,name,email세키를유지한다. 함수호출→새PublicProfile객체는id,name두키만복사한다. 두결과간전환간선은없으며외부입력검증이나깊은복사가아니다.타입으로 참조StoredProfileid · name · emailPublicProfile 대입typedView = 원본같은 실제 객체실제 키 3개 그대로허용 키로 생성같은 원본 객체id · name · emailtoPublicProfile 호출id · name만 복사새 실제 객체실제 키 2개
타입으로 보는 경로와 실제 객체 생성 경로 · 모바일동일StoredProfile에서서로독립인두3노드2간선경로를비교한다. 타입대입→typedView동일객체는실제id,name,email세키를유지한다. 함수호출→새PublicProfile객체는id,name두키만복사한다. 두결과간전환간선은없으며외부입력검증이나깊은복사가아니다. 모바일에서도 노드와 연결 방향·독립 경로가 같다.타입으로 참조StoredProfileid · name · emailPublicProfile 대입typedView = 원본같은 실제 객체실제 키 3개 그대로허용 키로 생성같은 원본 객체id · name · emailtoPublicProfile 호출id · name만 복사새 실제 객체실제 키 2개

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 타입을 선언해도 원본은 바뀌지 않는다
typedViewtypedView: 원본과 같은 객체 / 실제 키: 3개같은 객체를 PublicProfile 타입으로 참조한다타입에서 email을 보이지 않게 해도 runtime 키는 삭제되지 않는다
공개 값 생성public: 새 객체 키 2개 / 원본: 키 3개 유지toPublicProfile이 id와 name을 복사한 새 객체를 만든다allowlist 복사는 입력 검증이나 깊은 복사가 아니다

유틸리티 타입과 데이터 런타임 경계 — 상태 추적

실제 별도 실습에서 같은 원본을 typedView로 읽을 때 세 키가 남고 toPublicProfile 호출은 두 키의 새 객체를 만든다. 객체 정체성과 실제 키/별칭 변화로 정적 Omit이 삭제를 하지 않음을 보여 줄 가치가 있다.

본문의 유틸리티 타입은 정적 속성 규칙을 바꾼다. Required는 값을 채우지 않고 Readonly는 객체를 동결하지 않으며 Omit은 실제 속성을 삭제하지 않는다.

TYPE / RUNTIME / CONTRACT

유틸리티 타입을 적용해도 값은 저절로 바뀌지 않는다

Partial, Required, Readonly, Pick, Omit은 새로운 타입을 계산합니다. 초기값 생성, 객체 복사, 속성 제거는 실행 코드의 역할입니다.

타입으로 보는 경로와 실제 객체 생성 경로동일StoredProfile에서서로독립인두3노드2간선경로를비교한다. 타입대입→typedView동일객체는실제id,name,email세키를유지한다. 함수호출→새PublicProfile객체는id,name두키만복사한다. 두결과간전환간선은없으며외부입력검증이나깊은복사가아니다.타입으로 참조StoredProfileid · name · emailPublicProfile 대입typedView = 원본같은 실제 객체실제 키 3개 그대로허용 키로 생성같은 원본 객체id · name · emailtoPublicProfile 호출id · name만 복사새 실제 객체실제 키 2개
타입으로 보는 경로와 실제 객체 생성 경로 · 모바일동일StoredProfile에서서로독립인두3노드2간선경로를비교한다. 타입대입→typedView동일객체는실제id,name,email세키를유지한다. 함수호출→새PublicProfile객체는id,name두키만복사한다. 두결과간전환간선은없으며외부입력검증이나깊은복사가아니다. 모바일에서도 노드와 연결 방향·독립 경로가 같다.타입으로 참조StoredProfileid · name · emailPublicProfile 대입typedView = 원본같은 실제 객체실제 키 3개 그대로허용 키로 생성같은 원본 객체id · name · emailtoPublicProfile 호출id · name만 복사새 실제 객체실제 키 2개

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 타입을 선언해도 원본은 바뀌지 않는다
typedViewtypedView: 원본과 같은 객체 / 실제 키: 3개같은 객체를 PublicProfile 타입으로 참조한다타입에서 email을 보이지 않게 해도 runtime 키는 삭제되지 않는다
공개 값 생성public: 새 객체 키 2개 / 원본: 키 3개 유지toPublicProfile이 id와 name을 복사한 새 객체를 만든다allowlist 복사는 입력 검증이나 깊은 복사가 아니다

위 그림은 같은 원래 객체에서 출발한 두 경로를 단계별로 비교합니다. 타입으로만 바꾸어 본 경로는 같은 객체를 가리키고, 함수를 거친 경로는 두 키만 가진 새 객체를 만듭니다. 이 함수는 원시 값 세 개를 옮기는 예이며, 바깥에서 들어온 값을 검증하거나 중첩된 객체를 깊게 복사해 주지는 않습니다. 그림 아래쪽 카드의 FullProductConfig와 ImmutablePoint는 본문의 fullConfig와 fixedPoint이며, 본문의 userUpdate는 그림과 달리 email 없이 name만 담습니다. 각 유틸리티의 정의는 공식 유틸리티 타입 목록에 있습니다.

더 깊이: 표준 라이브러리의 선언에서 읽을 수 있는 것

표준 유틸리티는 컴파일러와 함께 배포되는 lib.es5.d.ts에 타입 선언으로 들어 있어서, 선언을 읽으면 이름만으로는 알 수 없는 차이가 보입니다. 대표적인 예가 Pick과 Omit입니다. Pick<T, K extends keyof T>는 K를 T의 키로 제한하므로 없는 키를 적으면 진단되지만, Omit<T, K extends keyof any>는 K를 문자열과 숫자, 심벌 가운데 아무 키로나 받습니다. 그래서 Omit<Todo, "descriptoin">처럼 키의 철자를 틀리면 진단 없이 아무 속성도 빼지 않은 타입이 됩니다.

ReturnType과 Parameters는 조건부 타입을 다룬 글에서 본 것처럼 T에 (...args: any) => any라는 제약을 둔 조건부 타입입니다. Awaited는 await의 동작을 흉내 내도록 재귀적으로 선언되어 있는데, null과 undefined는 그대로 돌려주고, then 메서드를 가진 객체(thenable)라면 그 then이 콜백에 넘기는 값의 타입을 다시 Awaited로 풉니다. 선언의 주석은 Promise가 아닌 thenable은 never가 되어야 한다고 적고 있으며, 실제 선언은 then의 첫 매개변수가 함수 모양이 아닐 때 never를 고릅니다.

모든 유틸리티를 타입 문법으로 적을 수 있는 것은 아닙니다. 매핑된 타입을 다룬 글에서 쓴 Capitalize와 같은 계열의 Uppercase, Lowercase, Uncapitalize, 그리고 3장에서 본 NoInfer는 선언의 오른쪽이 intrinsic이라는 키워드로 되어 있는데, 이는 계산 규칙이 타입 선언이 아니라 컴파일러 안에 구현되어 있다는 표시입니다. 그래서 이 유틸리티들은 선언을 읽어도 계산 규칙이 보이지 않고, 동작은 문서와 릴리스 노트로 확인해야 합니다.

정리

  • Partial, Required, Readonly는 속성의 규칙을 바꾸고, Pick, Omit, Record는 남길 키나 키마다의 값을 정합니다.
  • Exclude, Extract, NonNullable은 유니온의 멤버를 걸러 내고, Parameters, ReturnType, ThisParameterType, OmitThisParameter는 함수 타입을 다룹니다.
  • Awaited는 Promise를 끝까지 풀어 await가 돌려줄 값의 타입을 계산합니다.
  • 유틸리티 타입은 실제 객체를 바꾸지 않으므로, 속성을 고른 객체가 필요하면 그 속성만 읽어 새 객체를 만드는 코드를 따로 씁니다.

연습 문제

1. 공개할 키 바꾸기

공개 응답을 id와 email 두 속성으로 바꾸려 합니다. PublicProfile과 toPublicProfile을 각각 어떻게 고쳐야 하나요? 고친 뒤 publicProfile과 storedProfile의 키는 각각 무엇인가요?

해설 보기

타입은 Pick<StoredProfile, "id" | "email">로 바꾸고, 함수의 반환 객체도 { id: profile.id, email: profile.email }로 바꿉니다. 타입만 바꾸고 함수를 그대로 두면, 반환 객체에 email이 없고 타입에 없는 name이 들어 있어 진단됩니다. 고친 뒤 publicProfile의 키는 id,email이고, 원래 객체 storedProfile은 여전히 id,name,email입니다.

2. 새 속성이 생겼을 때

StoredProfile에 passwordHash: string이 더해졌습니다. 공개 타입을 Pick<StoredProfile, "id" | "name">로 정의했을 때와 Omit<StoredProfile, "email">로 정의했을 때, 공개 타입에는 각각 어떤 속성이 있나요?

해설 보기

Pick으로 정의한 공개 타입은 고른 두 키만 남기므로 여전히 id와 name뿐입니다. Omit으로 정의한 공개 타입은 email만 빼고 나머지를 모두 남기므로, 새로 생긴 passwordHash까지 공개 타입에 들어갑니다. 공개할 속성의 목록을 정하는 일에는 허용할 키를 직접 적는 Pick이 더 안전합니다.

3. 비동기 함수의 결과 타입

async function loadNames() { return ["민지", "준호"]; }가 있을 때, ReturnType<typeof loadNames>와 Awaited<ReturnType<typeof loadNames>>는 각각 어떤 타입인가요?

해설 보기

async 함수는 항상 Promise를 돌려주므로, 반환 타입을 적지 않았어도 ReturnType<typeof loadNames>는 Promise<string[]>입니다. 여기에 Awaited를 씌우면 Promise가 풀려 string[]가 되며, await loadNames()가 돌려주는 값의 타입과 같습니다.

이것으로 5장의 고급 타입을 마칩니다. 다음 장의 제네릭 제약 조건에서는 3장에서 본 제네릭을 다시 꺼내, 제약 조건을 더 다양하게 쓰는 방법을 다룹니다.