TypeScriptMedium#11
TypeScript의 유틸리티 타입(Utility Types) 중 자주 사용하는 것들을 설명해주세요.
#TS#유틸리티타입#타입
힌트
Partial, Required, Pick, Omit, Readonly, Record 등을 생각해보세요.
정답 및 해설
TypeScript의 유틸리티 타입(Utility Types) 중 자주 사용하는 것들을 설명해주세요.
TypeScript는 일반적인 타입 변환 작업을 위해 유틸리티 타입(Utility Types) 을 내장으로 제공합니다. 이를 활용하면 기존 타입을 기반으로 새로운 타입을 간결하게 정의할 수 있어 코드 중복을 줄이고 타입 시스템을 더 효율적으로 활용할 수 있습니다.
자주 사용하는 유틸리티 타입 한눈에 보기
| 유틸리티 타입 | 설명 |
|---|---|
Partial<T> | 모든 프로퍼티를 선택적으로 |
Required<T> | 모든 프로퍼티를 필수로 |
Readonly<T> | 모든 프로퍼티를 읽기 전용으로 |
Pick<T, K> | 특정 프로퍼티만 선택 |
Omit<T, K> | 특정 프로퍼티 제외 |
Record<K, V> | 키-값 쌍의 객체 타입 |
Exclude<T, U> | 유니온에서 특정 타입 제외 |
Extract<T, U> | 유니온에서 특정 타입만 추출 |
NonNullable<T> | null과 undefined 제거 |
ReturnType<T> | 함수의 반환 타입 추출 |
Parameters<T> | 함수의 매개변수 타입 추출 |
InstanceType<T> | 생성자 함수의 인스턴스 타입 |
Partial<T>
모든 프로퍼티를 **선택적(optional)**으로 만듭니다.
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
// PATCH API — 일부 필드만 업데이트
type UpdateUserDto = Partial<User>;
// { id?: number; name?: string; email?: string; role?: 'admin' | 'user' }
async function updateUser(id: number, updates: Partial<User>): Promise<User> {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
return response.json();
}
updateUser(1, { name: '새 이름' }); // id, email, role 없어도 OK
Required<T>
모든 프로퍼티를 **필수(required)**로 만듭니다. 선택적 프로퍼티(?)를 모두 제거합니다.
interface Config {
host?: string;
port?: number;
timeout?: number;
}
// 기본값이 적용된 후의 완전한 설정 타입
type ResolvedConfig = Required<Config>;
// { host: string; port: number; timeout: number }
function createServer(config: ResolvedConfig) {
// 모든 프로퍼티가 존재함을 보장
console.log(`${config.host}:${config.port}`);
}
Readonly<T>
모든 프로퍼티를 **읽기 전용(readonly)**으로 만듭니다.
interface Point {
x: number;
y: number;
}
const origin: Readonly<Point> = { x: 0, y: 0 };
// origin.x = 1; // 에러: 읽기 전용 프로퍼티에 할당 불가
// 불변 상태 관리에 유용
type ImmutableState = Readonly<{
users: readonly User[];
selectedId: number | null;
}>;
Pick<T, K>
타입 T에서 지정한 프로퍼티 K만 선택하여 새 타입을 만듭니다.
interface Article {
id: number;
title: string;
content: string;
author: string;
publishedAt: Date;
tags: string[];
}
// 목록 화면에 필요한 필드만
type ArticlePreview = Pick<Article, 'id' | 'title' | 'author' | 'publishedAt'>;
// { id: number; title: string; author: string; publishedAt: Date }
// 폼 데이터에 필요한 필드만
type ArticleForm = Pick<Article, 'title' | 'content' | 'tags'>;
function renderList(articles: ArticlePreview[]) {
articles.forEach((a) => {
console.log(a.title, a.author);
// a.content; // 에러: Pick에 포함되지 않음
});
}
Omit<T, K>
타입 T에서 지정한 프로퍼티 K를 제외하여 새 타입을 만듭니다.
interface User {
id: number;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}
// API 응답에서 민감한 정보 제외
type PublicUser = Omit<User, 'passwordHash'>;
// { id: number; name: string; email: string; createdAt: Date }
// 생성 시 서버가 자동 설정하는 필드 제외
type CreateUserDto = Omit<User, 'id' | 'createdAt'>;
// { name: string; email: string; passwordHash: string }
Record<K, V>
키 타입 K와 값 타입 V로 객체 타입을 만듭니다.
// 상태 코드별 메시지
type StatusMessages = Record<number, string>;
const httpMessages: StatusMessages = {
200: 'OK',
404: 'Not Found',
500: 'Internal Server Error',
};
// 사용자 역할별 권한
type Role = 'admin' | 'editor' | 'viewer';
type Permission = 'read' | 'write' | 'delete';
type RolePermissions = Record<Role, Permission[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
editor: ['read', 'write'],
viewer: ['read'],
};
// id를 키로 하는 객체 (캐시, 룩업 테이블)
type UserMap = Record<string, User>;
const userCache: UserMap = {};
Exclude<T, U> / Extract<T, U>
유니온 타입을 필터링합니다.
type AllEvents = 'click' | 'focus' | 'blur' | 'keydown' | 'keyup';
// Exclude — U에 포함된 타입 제거
type NonKeyboardEvents = Exclude<AllEvents, 'keydown' | 'keyup'>;
// 'click' | 'focus' | 'blur'
// Extract — U에 포함된 타입만 추출
type KeyboardEvents = Extract<AllEvents, 'keydown' | 'keyup'>;
// 'keydown' | 'keyup'
// 유니온에서 null/undefined 제거 (NonNullable과 동일)
type MaybeString = string | null | undefined;
type DefiniteString = Exclude<MaybeString, null | undefined>; // string
NonNullable<T>
타입에서 null과 undefined를 제거합니다.
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User
function processUser(user: User | null) {
if (user == null) return;
// 이 시점에서 user는 User 타입
const definiteUser: NonNullable<typeof user> = user;
console.log(definiteUser.name);
}
ReturnType<T>
함수의 반환 타입을 추출합니다.
function createUser(name: string, email: string) {
return {
id: Math.random(),
name,
email,
createdAt: new Date(),
};
}
// 함수의 반환 타입을 별도로 정의하지 않아도 추출 가능
type UserFromFactory = ReturnType<typeof createUser>;
// { id: number; name: string; email: string; createdAt: Date }
// Redux Action Creator 패턴
const fetchUser = (id: number) => ({ type: 'FETCH_USER' as const, payload: id });
type FetchUserAction = ReturnType<typeof fetchUser>;
// { type: 'FETCH_USER'; payload: number }
Parameters<T>
함수의 매개변수 타입을 튜플로 추출합니다.
function createOrder(userId: number, items: string[], total: number) {
return { userId, items, total };
}
type CreateOrderParams = Parameters<typeof createOrder>;
// [userId: number, items: string[], total: number]
// 데코레이터 패턴에서 활용
function logCall<T extends (...args: any[]) => any>(fn: T) {
return (...args: Parameters<T>): ReturnType<T> => {
console.log('호출:', fn.name, args);
return fn(...args);
};
}
유틸리티 타입 조합
실무에서는 여러 유틸리티 타입을 조합하여 사용하는 경우가 많습니다.
interface Product {
id: number;
name: string;
price: number;
description: string;
stock: number;
createdAt: Date;
updatedAt: Date;
}
// 생성 시 — id, 날짜 제외, name과 price는 필수
type CreateProductDto = Required<Pick<Product, 'name' | 'price'>> &
Partial<Omit<Product, 'id' | 'name' | 'price' | 'createdAt' | 'updatedAt'>>;
// 수정 시 — id, 날짜 제외, 나머지는 선택적
type UpdateProductDto = Partial<Omit<Product, 'id' | 'createdAt' | 'updatedAt'>>;
유틸리티 타입을 잘 활용하면 타입 정의의 중복을 줄이고 단일 진실 공급원(Single Source of Truth)을 유지할 수 있습니다.