전체 목록
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>

타입에서 nullundefined를 제거합니다.

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)을 유지할 수 있습니다.