유틸리티 타입은 타입스크립트가 자체적으로 제공하는 특수한 타입들로, 제네릭, 맵드 타입, 조건부 타입 등을 이용해 실무에서 자주 사용되는 타입들을 모아 놓은 것이다.
예를 들어 Readonly<T>로 모든 프로퍼티를 읽기 전용으로 만들 수 있다.
interface Person {
name: string;
hp: number;
}
const person: Readonly<Person> = {
name: "devmark",
hp: 30
};
person.name = ''; // ❌ 읽기 전용 프로퍼티
Partial<T>로 모든 프로퍼티를 선택적으로 만들 수도 있다.
const person: Partial<Person> = {
name: "devmark",
};
이 글에서는 실무에서 자주 사용되는 유틸리티 타입들을 직접 구현하며 동작 원리를 이해해보려고 한다.
더 많은 유틸리티 타입은 TypeScript 공식 문서에서 확인할 수 있다.
맵드 타입 기반 유틸리티 타입
[TS] 맵드 타입
맵드 타입(Mapped Type)은 기존 객체 타입을 기반으로 새로운 객체 타입을 만드는 기능이다. 유저 정보를 관리하는 함수들을 작성해보자.interface User { id: number; name: string; age: number;}function fetchUser(): U
devmark.tistory.com
Partial
Partial<T>는 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 변환한다.
블로그 플랫폼을 구현한다고 가정해보자.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const draft: Post = { // ❌ tags 프로퍼티가 없음
title: "제목은 나중에 짓자...",
content: "초안...",
};
임시 저장 게시글은 일부 정보가 아직 설정되지 않았다. 그렇다고 Post 타입의 모든 프로퍼티를 선택적으로 만들면 완성된 게시글도 불완전한 상태를 허용하게 된다.
Partial을 적용해보자.
const draft: Partial<Post> = {
title: "제목 나중에 짓자",
content: "초안...",
};

직접 구현해보기
일단 하나의 타입 변수 T를 사용하는 제네릭 타입인 것은 확실하다.
type Partial<T> = any;
다음으로는 T에 할당된 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 바꿔줘야 한다.
기존 객체 타입을 다른 타입으로 변환하는 맵드 타입을 이용해 다음과 같이 수정한다.
type Partial<T> = {
[key in keyof T]?: T[key];
}

Required
Required<T>는 객체 타입의 모든 프로퍼티를 필수 프로퍼티로 변환한다.
썸네일이 반드시 있어야 하는 게시글이 필요하다고 가정해보자.
const withThumbnailPost: Post = {
title: "유틸리티 타입",
tags: ["ts"],
content: "",
thumbnailURL: "https://...",
};
Post의 thumbnailURL이 선택적 프로퍼티라서 실수로 삭제해도 오류가 발생하지 않는다.

Required를 적용해보자.
const withThumbnailPost: Required<Post> = {
title: "유틸리티 타입",
tags: ["ts"],
content: "",
thumbnailURL: "https://...", // 필수
};

직접 구현해보기
일단 기존의 모든 프로퍼티를 포함하는 제네릭 맵드 타입으로 만들어준다.
type Required<T> = {
[key in keyof T]: T[key];
};
그리고 이제 모든 프로터피가 필수 프로퍼티가 되도록 만들어야 한다.
모든 프로퍼티를 필수 프로퍼티로 만든다는 것은, 반대로 모든 프로퍼티에서 "선택적"이라는 기능을 제거하는 것과 같다.
따라서 다음과 같이 -?를 프로퍼티 이름 뒤에 붙여주면 된다.
type Required<T> = {
[key in keyof T]-?: T[key];
};
-?는 ?가 붙어있는 선택적 프로퍼티가 있으면 ?를 제거하라는 의미이다.

Readonly
Readonly<T>는 객체 타입의 모든 프로퍼티를 읽기 전용으로 변환한다.
절대 수정할 수 없는 보호된 게시글이 필요하다고 가정해보자.
const readonlyPost: Post = {
title: "보호된 게시글입니다.",
tags: [],
content: "",
};
readonlyPost.content = '해킹당함'; // 막을 수 없음
Readonly를 적용해보자.
const readonlyPost: Readonly<Post> = {
title: "보호된 게시글입니다.",
tags: [],
content: "",
};
readonlyPost.content = '해킹당함'; // ❌

직접 구현해보기
type Readonly<T> = {
readonly [key in keyof T]: T[key];
};

Pick
Pick<T, K>는 객체 타입에서 특정 프로퍼티만 골라낸다.
태그 기능이 추가되기 전에 작성된 옛날 게시글이 있다고 가정해보자.
const legacyPost: Post = { // ❌ tags 프로퍼티 필요
title: "옛날 글",
content: "옛날 컨텐츠",
};
옛날 게시글만을 위한 타입을 별도로 만들 수도 없고, 일일이 tags를 추가할 수도 없다.
Pick을 적용해보자.
const legacyPost: Pick<Post, "title" | "content"> = {
title: "옛날 글",
content: "옛날 컨텐츠",
};

직접 구현해보기
객체 타입을 변형하는 타입이므로 맵드 타입을 이용해 만들 수 있다.
일단 2개의 타입 변수 T와 K를 사용하는 타입이므로 다음과 같이 정의한다.
type Pick<T, K> = any;
T로부터 K프로퍼티만 뽑아낸 객체 타입을 만들어야 하므로 다음과 같이 맵드 타입으로 정의한다.
type Pick<T, K> = {
[key in K]: T[key];
};
마지막으로 K가 T의 key로만 이루어진 String Literal Union타입임을 보장할 수 있도록 제약을 추가한다.
type Pick<T, K extends keyof T> = {
[key in K]: T[key];
};

Omit
Omit<T, K>는 객체 타입에서 특정 프로퍼티만 제거한다.
제목이 없는 게시글도 존재할 수 있다고 가정해보자.
const noTitlePost: Post = { // ❌ title 프로퍼티 필요
content: "",
tags: [],
thumbnailURL: "",
};
Omit을 적용해보자.
const noTitlePost: Omit<Post, "title"> = {
content: "",
tags: [],
thumbnailURL: "",
};

직접 구현해보기
먼저 2개의 타입 변수를 사용하는 제네릭 타입이므로 다음과 같이 정의한다.
type Omit<T, K> = any;
그 다음 앞서 Pick타입에서 했던 것과 같이 K에 제약을 추가한다.
type Omit<T, K extends keyof T> = any;
그리고 앞서 만든 Pick타입을 이용해 다음과 같이 완성한다.
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

Omit<Post, "title">의 동작 과정:
keyof Post→"title" | "content" | "tags" | "thumbnailURL"Exclude<"title" | "content" | "tags" | "thumbnailURL", "title">→"content" | "tags" | "thumbnailURL"Pick<Post, "content" | "tags" | "thumbnailURL">→title이 제거된 타입
Record
Record<K, V>는 키와 값의 타입을 지정하여 객체 타입을 생성한다.
화면 크기에 따라 3가지 버전의 썸네일을 지원한다고 가정해보자.
type Thumbnail = {
large: {
url: string;
};
medium: {
url: string;
};
small: {
url: string;
};
};
이 코드는 버전이 추가될때마다 중복 코드가 발생하게 된다.
Record를 적용해보자.
type Thumbnail = Record
"large" | "medium" | "small",
{ url: string }
>;


직접 구현해보기
type Record<K extends keyof any, V> = {
[key in K]: V;
};
K extends keyof any는 K가 객체의 키로 사용할 수 있는 타입(string, number, symbol)임을 보장한다.

조건부 타입 기반 유틸리티 타입
[TS] 분산적인 조건부 타입
분산적인 조건부 타입type StringNumberSwitch = T extends number ? string : number;let a: StringNumberSwitch; // stringlet b: StringNumberSwitch; // number 위 조건부 타입에 Union을 할당해 보자.let c: StringNumberSwitch; // string | numb
devmark.tistory.com
Exclude
Exclude<T, U>는 Union 타입 T에서 U를 제거한다.
type A = Exclude<string | boolean, string>; // boolean
조건부 타입과 분산을 이용해 구현할 수 있다.
type Exclude<T, U> = T extends U ? never : T;

Extract
Extract<T, U>는 Union 타입 T에서 U만 추출한다.
type B = Extract<string | boolean, boolean>; // boolean
직접 구현하면 이런 모습이다.
type Extract<T, U> = T extends U ? T : never;

ReturnType
ReturnType<T>는 함수 타입의 반환값 타입을 추출한다.
function funcA() {
return "hello";
}
function funcB() {
return 10;
}
type ReturnA = ReturnType<typeof funcA>; // string
type ReturnB = ReturnType<typeof funcB>; // number
직접 구현하자면, infer를 사용해 반환 타입을 추론할 수 있다.
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: never;

'Frontend' 카테고리의 다른 글
| Zustand로 전역 상태 관리하기 (0) | 2025.11.18 |
|---|---|
| aria-label과 텍스트 콘텐츠 (0) | 2025.11.13 |
| 웹 애플리케이션의 렌더링 방식과 Next.js (0) | 2025.11.08 |
| 타입스크립트 타입 시스템: 계층과 호환성 (0) | 2025.10.31 |
| 타입스크립트의 동작 원리 (0) | 2025.10.27 |
