DevToolBox무료
블로그

TypeScript 유틸리티 타입 치트시트: Partial, Pick, Omit 등

13분 읽기by DevToolBox

TypeScript 유틸리티 타입 완벽 가이드

TypeScript 유틸리티 타입은 기존 타입을 새로운 타입으로 변환하는 내장 제네릭 타입입니다.

Partial, Required 및 Readonly

These three utility types transform the mutability and optionality of every property at once. They are the most commonly used utility types in TypeScript codebases.

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

// Partial<T> - makes all properties optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number }

function updateUser(id: number, data: Partial<User>): User {
  return { ...getCurrentUser(id), ...data };
}
updateUser(1, { name: 'Alice' }); // OK - only update name

// Required<T> - makes all properties required
interface Config {
  host?: string;
  port?: number;
  timeout?: number;
}
type StrictConfig = Required<Config>;
// { host: string; port: number; timeout: number }

// Readonly<T> - makes all properties readonly
type ImmutableUser = Readonly<User>;
const user: ImmutableUser = { id: 1, name: 'Alice', email: 'a@b.com', age: 30 };
// user.name = 'Bob'; // Error: Cannot assign to 'name' because it is read-only

// Deep Readonly (custom utility)
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

Pick, Omit 및 Extract

Pick and Omit let you create new types by selecting or removing properties. Extract and Exclude operate on union types. These are essential for creating DTOs (Data Transfer Objects) and API shapes.

interface Product {
  id: number;
  name: string;
  price: number;
  stock: number;
  category: string;
  description: string;
}

// Pick<T, K> - select only the specified keys
type ProductCard = Pick<Product, 'id' | 'name' | 'price'>;
// { id: number; name: string; price: number }

// Useful for API response shaping
function getProductCard(p: Product): ProductCard {
  return { id: p.id, name: p.name, price: p.price };
}

// Omit<T, K> - remove specified keys
type ProductForm = Omit<Product, 'id' | 'stock'>;
// { name: string; price: number; category: string; description: string }

// Exclude<T, U> - from union type, exclude assignable to U
type Direction = 'north' | 'south' | 'east' | 'west';
type Horizontal = Exclude<Direction, 'north' | 'south'>;
// 'east' | 'west'

// Extract<T, U> - from union type, keep only assignable to U
type Vertical = Extract<Direction, 'north' | 'south'>;
// 'north' | 'south'

// NonNullable<T> - removes null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string

Record 및 매핑된 타입

Record creates object types with a fixed set of keys. Mapped types iterate over union types or object keys to transform every property systematically.

// Record<K, V> - create object type with known keys
type Role = 'admin' | 'user' | 'guest';
type Permissions = Record<Role, string[]>;

const permissions: Permissions = {
  admin: ['read', 'write', 'delete'],
  user:  ['read', 'write'],
  guest: ['read'],
};

// Record with nested types
type ApiResponse<T> = Record<string, T>;
type UserMap = Record<number, User>;

// Mapped Types - transform every property
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Optional<T> = { [K in keyof T]?: T[K] };
type Stringify<T> = { [K in keyof T]: string };

// Mapped types with modifiers
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // remove readonly
type Complete<T> = { [K in keyof T]-?: T[K] };         // remove optional

// Remapping keys with 'as'
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; ... }

ReturnType 및 함수 유틸리티

Function utility types let you extract type information from functions and classes without duplicating type definitions. They're especially useful when working with third-party libraries that don't export their types.

// ReturnType<T> - extract return type of a function
function createUser(name: string, email: string) {
  return { id: Math.random(), name, email, createdAt: new Date() };
}
type CreatedUser = ReturnType<typeof createUser>;
// { id: number; name: string; email: string; createdAt: Date }

// Parameters<T> - extract parameter types as tuple
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, email: string]

// ConstructorParameters<T> - for class constructors
class Server {
  constructor(public host: string, public port: number) {}
}
type ServerArgs = ConstructorParameters<typeof Server>;
// [host: string, port: number]

// InstanceType<T> - get instance type from constructor
type ServerInstance = InstanceType<typeof Server>;
// Server

// Awaited<T> - unwrap Promise types
type UserPromise = Promise<User>;
type ResolvedUser = Awaited<UserPromise>;
// User

async function fetchUser(): Promise<User> { /* ... */ return {} as User; }
type FetchResult = Awaited<ReturnType<typeof fetchUser>>;
// User

조건부 타입과 Infer

Conditional types express if-else logic at the type level. The infer keyword lets you capture and name types within conditional expressions, enabling powerful type extraction patterns.

// Conditional Types
type IsArray<T> = T extends any[] ? true : false;
type CheckString = IsArray<string>;  // false
type CheckArray  = IsArray<string[]>; // true

// infer - extract type from generic
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type StringItem = UnpackArray<string[]>; // string
type NumberItem  = UnpackArray<number>;  // number

// Extract Promise value
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type PromiseValue = UnpackPromise<Promise<number>>; // number

// Get first element of tuple
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Head = First<[string, number, boolean]>; // string

// Distributive conditional types
type Flatten<T> = T extends Array<infer U> ? U : T;
type Mixed = Flatten<string[] | number | boolean[]>;
// string | number | boolean

유틸리티 타입 조합

The real power of utility types comes from combining them. Here's a real-world example showing how to build a complete API type system from a single source type:

// Real-world example: API layer with combined utility types

interface ApiUser {
  id: number;
  username: string;
  email: string;
  password: string;   // should never leave server
  createdAt: string;
  updatedAt: string;
}

// Safe user for frontend (strip sensitive fields)
type SafeUser = Omit<ApiUser, 'password'>;

// User creation form (no server-generated fields)
type CreateUserDto = Omit<ApiUser, 'id' | 'createdAt' | 'updatedAt'>;

// Patch request - all fields optional except id
type PatchUserDto = Partial<Omit<ApiUser, 'id'>> & Pick<ApiUser, 'id'>;

// Validation errors map
type ValidationErrors<T> = Partial<Record<keyof T, string>>;
type UserErrors = ValidationErrors<CreateUserDto>;
// { username?: string; email?: string; password?: string }

// Generic API response wrapper
type ApiResult<T, E = string> =
  | { success: true; data: T }
  | { success: false; error: E };

type UserResult = ApiResult<SafeUser>;

자주 묻는 질문

What is the difference between Partial<T> and Optional<T>?

Partial<T> is a built-in TypeScript utility type that makes all properties of T optional. Optional<T> is not built-in — it's a common name for custom mapped types that do the same thing. You should always prefer the built-in Partial<T> over writing your own.

When should I use Pick vs Omit?

Use Pick when you want to select a small subset of properties from a large type (you know what you WANT). Use Omit when you want most properties but need to exclude a few (you know what you DON'T WANT). For removing 1-2 properties, Omit is cleaner. For selecting 1-2 properties, Pick is cleaner.

What is the difference between Record<string, T> and { [key: string]: T }?

They are nearly identical in behavior, but Record<string, T> is more readable and the idiomatic TypeScript style. Record<string, T> also plays better with other utility types. One subtle difference: Record<never, T> produces an empty object type {}, while the index signature always allows any string key.

How do I create a deep Partial type?

TypeScript's built-in Partial<T> is shallow — it only makes the top level optional. For deep partial, create a recursive utility type: type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }. Libraries like ts-toolbelt provide this and many more deep utility types.

What are template literal types and how do they relate to mapped types?

Template literal types (introduced in TS 4.1) let you create string types with embedded type expressions: type EventName<T extends string> = `on${Capitalize<T>}`. Combined with mapped types, you can generate entire interfaces of event handlers, getters, setters, etc., from a single source-of-truth type.

Can I use utility types with interfaces and type aliases both?

Yes. TypeScript utility types work with both interface and type alias definitions interchangeably. Partial<MyInterface> and Partial<MyType> both work the same way. The choice between interface and type alias is mostly stylistic, though interfaces support declaration merging while type aliases do not.

결론

TypeScript utility types are force multipliers for your type system. Instead of maintaining dozens of similar interfaces that drift out of sync, define one source-of-truth type and derive all variants with Partial, Pick, Omit, and friends. Your codebase becomes easier to maintain and refactor as requirements change.

Start by auditing your codebase for interfaces that are near-duplicates of each other — those are prime candidates for utility type refactoring. Tools like the viadreams.cc JSON formatter and diff tool can help you compare complex type structures visually.

관련 도구

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

TSJSON to TypeScriptZDJSON to Zod Schema{ }JSON Formatter

Related Articles

TypeScript 제네릭 완전 가이드: 실전 예제로 배우기

기초부터 고급 패턴까지 TypeScript 제네릭을 마스터하세요. 함수, 인터페이스, 제약조건, 조건부 타입을 다룹니다.

TypeScript vs JavaScript: 언제, 어떻게 변환할까

TypeScript를 JavaScript로, 또는 그 반대로 변환해야 할 시기에 대한 실전 가이드. 마이그레이션 전략, 도구, 번들 크기 영향, 팀 고려사항을 다룹니다.

JSON에서 TypeScript로: 예제와 함께하는 완벽 가이드

JSON 데이터를 TypeScript 인터페이스로 자동 변환하는 방법을 배웁니다. 중첩 객체, 배열, 선택적 필드, 모범 사례를 다룹니다.