7.5 커스텀 유틸리티 타입 만들기
TypeScript 내장 유틸리티 타입(Partial, Required, Omit 등)은 많은 경우를 처리하지만, 실무에서는 더 정교한 타입 변환이 필요합니다. 이 장에서는 직접 구현해서 사용하는 강력한 커스텀 유틸리티 타입들을 다룹니다. 이 타입들을 익히면 타입을 데이터처럼 다루는 "타입 레벨 프로그래밍"의 진수를 경험할 수 있습니다.
DeepPartial — 중첩 객체까지 모두 optional
내장 Partial<T>는 한 단계 깊이만 optional로 만듭니다. 중첩된 객체 프로퍼티까지 모두 optional로 만들려면 재귀 타입이 필요합니다.
// 내장 Partial의 한계
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
certPath: string;
};
};
database: {
host: string;
port: number;
};
}
type ShallowPartial = Partial<Config>;
// server?: { host: string; port: number; ssl: {...} } ← server는 optional이지만 내부는 아님
// DeepPartial 구현
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
type DeepConfig = DeepPartial<Config>;
// server?: { host?: string; port?: number; ssl?: { enabled?: boolean; certPath?: string } }
// 실용 예: 설정 병합
function mergeConfig(base: Config, override: DeepPartial<Config>): Config {
return deepMerge(base, override) as Config;
}
function deepMerge<T extends object>(target: T, source: DeepPartial<T>): T {
const result = { ...target };
for (const key in source) {
const sourceVal = source[key as keyof typeof source];
const targetVal = target[key as keyof T];
if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal)) {
(result as any)[key] = deepMerge(targetVal as any, sourceVal as any);
} else if (sourceVal !== undefined) {
(result as any)[key] = sourceVal;
}
}
return result;
}
// 배열과 함수는 재귀하지 않도록 개선된 버전
type DeepPartialStrict<T> =
T extends (infer U)[]
? DeepPartialStrict<U>[]
: T extends Function
? T
: T extends object
? { [P in keyof T]?: DeepPartialStrict<T[P]> }
: T;
DeepReadonly — 중첩 객체까지 모두 readonly
// 내장 Readonly의 한계: 한 단계만 불변
type ShallowReadonly = Readonly<Config>;
// server: { readonly ... } 이지만 server.ssl.enabled는 여전히 변경 가능
// DeepReadonly 구현
type DeepReadonly<T> =
T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends Map<infer K, infer V>
? ReadonlyMap<K, DeepReadonly<V>>
: T extends Set<infer V>
? ReadonlySet<DeepReadonly<V>>
: T extends Function
? T
: T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
type ImmutableConfig = DeepReadonly<Config>;
// 모든 중첩 프로퍼티가 readonly
// 실용 예: 애플리케이션 상태 불변성 보장
interface AppState {
user: {
id: string;
profile: {
name: string;
avatar: string;
preferences: {
theme: "light" | "dark";
language: string;
};
};
};
cart: {
items: Array<{ productId: string; quantity: number; price: number }>;
total: number;
};
}
type ImmutableState = DeepReadonly<AppState>;
// 컴파일 에러 — 불변 상태 보호
// function mutateState(state: ImmutableState): void {
// state.user.profile.name = "hacked"; // Error!
// state.cart.items.push({ ... }); // Error!
// }
// 올바른 방식: 새 객체 반환
function updateUserName(state: ImmutableState, name: string): ImmutableState {
return {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
name,
},
},
};
}
OmitByValue — 특정 값 타입의 프로퍼티 제거
Omit<T, K>는 키 이름으로 제거하지만, OmitByValue<T, V>는 값의 타입이 V인 프로퍼티를 모두 제거합니다.
// OmitByValue 구현
type OmitByValue<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? never : K]: T[K];
};
// PickByValue: 반대로 값 타입이 V인 프로퍼티만 선택
type PickByValue<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
interface MixedInterface {
id: number;
name: string;
email: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
age: number | undefined;
}
// boolean 프로퍼티 제거
type NoBoolean = OmitByValue<MixedInterface, boolean>;
// { id, name, email, createdAt, updatedAt, deletedAt, age }
// string 프로퍼티만 선택
type OnlyStrings = PickByValue<MixedInterface, string>;
// { name: string; email: string }
// null/undefined 허용 프로퍼티 제거
type NoNullable = OmitByValue<MixedInterface, null | undefined>;
// { id, name, email, isActive, createdAt, updatedAt }
// Date 프로퍼티만 선택
type OnlyDates = PickByValue<MixedInterface, Date>;
// { createdAt: Date; updatedAt: Date }
// 실용 예: 직렬화 가능한 타입만 남기기
type JsonSerializable = string | number | boolean | null | JsonSerializable[] | {
[key: string]: JsonSerializable;
};
// 함수 프로퍼티 제거 (직렬화 준비)
type SerializableProps<T> = OmitByValue<T, Function>;
class UserViewModel {
id: string = "";
name: string = "";
email: string = "";
formatName(): string { return this.name; }
validate(): boolean { return true; }
}
type SerializableUser = SerializableProps<UserViewModel>;
// { id: string; name: string; email: string }
RequireAtLeastOne — 최소 하나의 프로퍼티 필수
검색 조건이나 필터처럼 여러 선택적 필드 중 최소 하나는 있어야 하는 경우에 사용합니다.
// RequireAtLeastOne 구현
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>> &
{
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
}[Keys];
// 검색 파라미터: id, email, username 중 최소 하나 필수
interface SearchParams {
id?: string;
email?: string;
username?: string;
includeDeleted?: boolean;
}
type ValidSearchParams = RequireAtLeastOne<SearchParams, "id" | "email" | "username">;
// 컴파일 에러 — 세 필드 모두 없음
// const bad: ValidSearchParams = { includeDeleted: true }; // Error!
// OK — 최소 하나 있음
const byId: ValidSearchParams = { id: "user-123" };
const byEmail: ValidSearchParams = { email: "alice@example.com" };
const byMultiple: ValidSearchParams = { id: "u1", email: "alice@example.com" };
// 실용 예: 알림 발송 (이메일 또는 SMS 중 하나 필수)
interface NotificationOptions {
title: string;
body: string;
email?: string;
smsPhone?: string;
pushToken?: string;
}
type SendNotification = RequireAtLeastOne<
NotificationOptions,
"email" | "smsPhone" | "pushToken"
>;
async function sendNotification(options: SendNotification): Promise<void> {
if (options.email) {
console.log(`Email to ${options.email}: ${options.title}`);
}
if (options.smsPhone) {
console.log(`SMS to ${options.smsPhone}: ${options.body}`);
}
if (options.pushToken) {
console.log(`Push to ${options.pushToken}: ${options.title}`);
}
}
RequireExactlyOne — 정확히 하나의 프로퍼티 필수
결제 수단처럼 여러 옵션 중 정확히 하나만 선택해야 하는 경우에 사용합니다.
// RequireExactlyOne 구현
type RequireExactlyOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>> &
{
[K in Keys]: Required<Pick<T, K>> & { [J in Exclude<Keys, K>]?: never };
}[Keys];
// 결제 수단: card, bankTransfer, crypto 중 정확히 하나
interface PaymentBase {
amount: number;
currency: string;
card?: { cardNumber: string; expiry: string; cvv: string };
bankTransfer?: { accountNumber: string; routingNumber: string };
crypto?: { walletAddress: string; coin: "BTC" | "ETH" | "USDT" };
}
type Payment = RequireExactlyOne<PaymentBase, "card" | "bankTransfer" | "crypto">;
// OK — 카드만 있음
const cardPayment: Payment = {
amount: 50000,
currency: "KRW",
card: { cardNumber: "1234-5678-9012-3456", expiry: "12/26", cvv: "123" },
};
// 컴파일 에러 — 두 개 동시에 지정
// const doublePayment: Payment = {
// amount: 50000,
// currency: "KRW",
// card: { ... },
// bankTransfer: { ... }, // Error! card가 있으면 bankTransfer는 never
// };
// 실용 예: 인증 방법 선택
interface AuthBase {
userId: string;
password?: string;
otp?: string;
biometric?: { type: "fingerprint" | "face"; data: string };
}
type AuthMethod = RequireExactlyOne<AuthBase, "password" | "otp" | "biometric">;
XOR — 배타적 OR 타입
두 타입 중 정확히 하나만 가능한 배타적 OR를 구현합니다.
// XOR 구현
type Without<T, U> = {
[P in Exclude<keyof T, keyof U>]?: never;
};
type XOR<T, U> = (T | U) extends object
? (Without<T, U> & U) | (Without<U, T> & T)
: T | U;
// 사각형 또는 원만 가능 (둘 다 X)
interface Rectangle {
width: number;
height: number;
}
interface Circle {
radius: number;
}
type Shape = XOR<Rectangle, Circle>;
const rect: Shape = { width: 10, height: 20 }; // OK
const circ: Shape = { radius: 5 }; // OK
// const both: Shape = { width: 10, height: 20, radius: 5 }; // Error!
// 실용 예: 토큰 인증 vs 세션 인증
interface TokenAuth {
token: string;
expiresAt: Date;
}
interface SessionAuth {
sessionId: string;
userId: string;
}
type AuthCredentials = XOR<TokenAuth, SessionAuth>;
function authenticate(credentials: AuthCredentials): Promise<boolean> {
if ("token" in credentials) {
// token 인증
return Promise.resolve(true);
} else {
// session 인증
return Promise.resolve(true);
}
}
// 실용 예 2: 컨트롤드 vs 언컨트롤드 컴포넌트 (React 패턴)
interface ControlledInput {
value: string;
onChange: (value: string) => void;
}
interface UncontrolledInput {
defaultValue?: string;
onBlur?: (value: string) => void;
}
type InputProps = XOR<ControlledInput, UncontrolledInput> & {
placeholder?: string;
disabled?: boolean;
};
// value만 있으면 onChange도 필수 (controlled)
// const bad: InputProps = { value: "hello" }; // Error: onChange 없음
const controlled: InputProps = { value: "hello", onChange: (v) => console.log(v) };
const uncontrolled: InputProps = { defaultValue: "world" };
Brand 타입 — 타입 안전한 ID
JavaScript/TypeScript에서 string은 모두 같은 타입이지만, UserId와 ProductId는 의미상 전혀 다릅니다. Brand 타입으로 구조적으로는 같지만 타입 시스템에서 구분되는 타입을 만들 수 있습니다.
// Brand 타입 구현
declare const __brand: unique symbol;
type Brand<T, BrandName extends string> = T & {
readonly [__brand]: BrandName;
};
// 도메인 ID 타입들
type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;
type OrderId = Brand<string, "OrderId">;
type CategoryId = Brand<string, "CategoryId">;
// ID 생성 함수 (유일한 Brand 값 생성 경로)
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
// 사용 예
const userId = createUserId("user-123");
const productId = createProductId("prod-456");
// 함수 시그니처가 타입 안전성을 보장
function getUserById(id: UserId): Promise<{ id: UserId; name: string }> {
return Promise.resolve({ id, name: "Alice" });
}
function getProductById(id: ProductId): Promise<{ id: ProductId; title: string }> {
return Promise.resolve({ id, name: "Widget" } as any);
}
// 타입 오류: UserId와 ProductId는 교환 불가
// getUserById(productId); // Error! ProductId는 UserId가 아님
getUserById(userId); // OK
// 숫자 기반 Brand
type Pixels = Brand<number, "Pixels">;
type Percentage = Brand<number, "Percentage">;
type Milliseconds = Brand<number, "Milliseconds">;
function px(value: number): Pixels { return value as Pixels; }
function pct(value: number): Percentage { return value as Percentage; }
function ms(value: number): Milliseconds { return value as Milliseconds; }
function setWidth(width: Pixels): void {
document.body.style.width = `${width}px`;
}
function setTimeout_safe(callback: () => void, delay: Milliseconds): void {
setTimeout(callback, delay);
}
setWidth(px(200)); // OK
// setWidth(pct(50)); // Error! Percentage는 Pixels가 아님
setTimeout_safe(() => {}, ms(1000)); // OK
Opaque 타입 패턴
Brand 타입의 확장으로, 완전히 불투명한(Opaque) 타입을 만들어 내부 구현을 숨깁니다.
// Opaque 타입: 생성과 접근을 완전히 제어
const OpaqueTag = Symbol("OpaqueTag");
type Opaque<T, Tag extends string> = T & {
readonly [OpaqueTag]: Tag;
};
// 비밀번호: 해시된 값임을 타입으로 보장
type HashedPassword = Opaque<string, "HashedPassword">;
type PlainPassword = Opaque<string, "PlainPassword">;
async function hashPassword(plain: PlainPassword): Promise<HashedPassword> {
// bcrypt 해싱 (실제 구현)
const hashed = `hashed_${plain}`; // 예시
return hashed as HashedPassword;
}
async function verifyPassword(
plain: PlainPassword,
hashed: HashedPassword
): Promise<boolean> {
return (await hashPassword(plain)) === hashed;
}
function createPlainPassword(raw: string): PlainPassword {
return raw as PlainPassword;
}
// 사용 흐름
const userInput = createPlainPassword("mySecret123");
// const dbHash = "hashed_value_from_db" as HashedPassword;
// 잘못된 사용 방지
// verifyPassword("plain_string", "hashed_string"); // Error!
// verifyPassword(userInput, userInput); // Error! 두 번째는 HashedPassword여야 함
// 검증된 이메일 타입
type ValidatedEmail = Opaque<string, "ValidatedEmail">;
function validateEmail(email: string): ValidatedEmail | null {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email) ? (email as ValidatedEmail) : null;
}
async function sendEmail(to: ValidatedEmail, subject: string, body: string): Promise<void> {
console.log(`Sending email to ${to}: ${subject}`);
}
// 검증 없이는 이메일 전송 불가
// sendEmail("invalid@", "Hi", "Body"); // Error!
const validEmail = validateEmail("alice@example.com");
if (validEmail) {
sendEmail(validEmail, "Welcome!", "Welcome to our service."); // OK
}
실전 예제 1: 폼 검증 유틸리티 타입
// 폼 필드 유효성 상태 타입
type FieldStatus = "untouched" | "valid" | "invalid";
interface FieldState<T> {
value: T;
status: FieldStatus;
error?: string;
}
// 폼 타입에서 자동으로 폼 상태 타입 생성
type FormState<T extends Record<string, unknown>> = {
[K in keyof T]: FieldState<T[K]>;
} & {
isValid: boolean;
isSubmitting: boolean;
isDirty: boolean;
};
// 검증 규칙 타입
type ValidationRule<T> = (value: T) => string | null;
type ValidationSchema<T extends Record<string, unknown>> = {
[K in keyof T]?: ValidationRule<T[K]>[];
};
// 폼 훅 반환 타입
interface UseFormReturn<T extends Record<string, unknown>> {
state: FormState<T>;
setValue: <K extends keyof T>(field: K, value: T[K]) => void;
validate: <K extends keyof T>(field: K) => boolean;
validateAll: () => boolean;
reset: () => void;
handleSubmit: (handler: (data: T) => Promise<void>) => (e: Event) => void;
}
// 실제 폼 데이터 타입
interface RegistrationForm {
username: string;
email: string;
password: string;
confirmPassword: string;
age: number;
agreeToTerms: boolean;
}
// 자동으로 생성되는 폼 상태 타입
type RegistrationFormState = FormState<RegistrationForm>;
/*
{
username: FieldState<string>;
email: FieldState<string>;
password: FieldState<string>;
confirmPassword: FieldState<string>;
age: FieldState<number>;
agreeToTerms: FieldState<boolean>;
isValid: boolean;
isSubmitting: boolean;
isDirty: boolean;
}
*/
// 검증 스키마 타입 안전성
const registrationSchema: ValidationSchema<RegistrationForm> = {
username: [
(v) => v.length >= 3 ? null : "3자 이상 입력해주세요",
(v) => /^[a-z0-9_]+$/.test(v) ? null : "영소문자, 숫자, _ 만 사용 가능합니다",
],
email: [
(v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? null : "올바른 이메일 형식이 아닙니다",
],
password: [
(v) => v.length >= 8 ? null : "8자 이상 입력해주세요",
(v) => /[A-Z]/.test(v) ? null : "대문자를 포함해야 합니다",
],
age: [
(v) => v >= 14 ? null : "14세 이상만 가입 가능합니다",
],
};
// 에러 메시지 추출 유틸리티
type FormErrors<T extends Record<string, unknown>> = {
[K in keyof T]?: string;
};
function extractErrors<T extends Record<string, unknown>>(
state: FormState<T>
): FormErrors<T> {
return Object.fromEntries(
Object.entries(state)
.filter(([key]) => key !== "isValid" && key !== "isSubmitting" && key !== "isDirty")
.filter(([, fieldState]) => (fieldState as FieldState<unknown>).error)
.map(([key, fieldState]) => [key, (fieldState as FieldState<unknown>).error])
) as FormErrors<T>;
}
실전 예제 2: 도메인 모델 ID 브랜딩
// 전체 도메인 ID 시스템 구축
declare const _brand: unique symbol;
type BrandedId<T extends string> = string & { readonly [_brand]: T };
// 도메인별 ID 타입
type UserId = BrandedId<"User">;
type PostId = BrandedId<"Post">;
type CommentId = BrandedId<"Comment">;
type TagId = BrandedId<"Tag">;
// ID 팩토리 (UUID 생성 포함)
function generateId<T extends string>(prefix: string): BrandedId<T> {
const uuid = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2);
return `${prefix}_${uuid}` as BrandedId<T>;
}
const newUserId = () => generateId<"User">("usr");
const newPostId = () => generateId<"Post">("pst");
const newCommentId = () => generateId<"Comment">("cmt");
// 도메인 엔티티 (ID 타입 강제)
interface User {
id: UserId;
name: string;
email: string;
postIds: PostId[];
}
interface Post {
id: PostId;
authorId: UserId;
title: string;
content: string;
commentIds: CommentId[];
tagIds: TagId[];
}
interface Comment {
id: CommentId;
postId: PostId;
authorId: UserId;
content: string;
}
// 리포지터리: ID 타입 안전성 보장
class UserRepository {
private users = new Map<UserId, User>();
save(user: User): void {
this.users.set(user.id, user);
}
findById(id: UserId): User | undefined {
return this.users.get(id);
}
// Error: PostId로 User 조회 불가
// findByPostId(id: PostId): User | undefined {
// return this.users.get(id); // Error! PostId는 UserId가 아님
// }
}
class PostRepository {
private posts = new Map<PostId, Post>();
findByAuthor(authorId: UserId): Post[] {
return [...this.posts.values()].filter(p => p.authorId === authorId);
}
// UserId와 PostId가 헷갈릴 수 없음
findById(id: PostId): Post | undefined {
return this.posts.get(id);
}
}
// 사용 예: 관계 안전성 보장
async function getUserWithPosts(
userId: UserId,
userRepo: UserRepository,
postRepo: PostRepository
): Promise<{ user: User; posts: Post[] } | null> {
const user = userRepo.findById(userId);
if (!user) return null;
const posts = postRepo.findByAuthor(userId);
return { user, posts };
}
// 실수 방지 예시
const uid = newUserId();
const pid = newPostId();
// getUserWithPosts(pid, userRepo, postRepo); // Error! pid는 PostId
getUserWithPosts(uid, new UserRepository(), new PostRepository()); // OK
고수 팁
커스텀 유틸리티 타입 라이브러리화
// utilities/types.ts — 프로젝트 유틸리티 타입 모음
export type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
export type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
export type OmitByValue<T, V> = {
[K in keyof T as T[K] extends V ? never : K]: T[K];
};
export type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
export type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>> &
{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys];
declare const _brand: unique symbol;
export type Brand<T, B extends string> = T & { readonly [_brand]: B };
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
export type Mutable<T> = { -readonly [P in keyof T]: T[P] };
export type DeepMutable<T> = T extends object
? { -readonly [P in keyof T]: DeepMutable<T[P]> }
: T;
// 함수 타입 유틸리티
export type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
export type AsyncReturnType<T extends (...args: any) => Promise<any>> =
Awaited<ReturnType<T>>;
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
// 튜플 유틸리티
export type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
export type Tail<T extends any[]> = T extends [any, ...infer T] ? T : never;
export type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
// 객체 유틸리티
export type ValueOf<T> = T[keyof T];
export type KeysOfType<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];
type-fest 소개
type-fest는 신뢰할 수 있는 TypeScript 유틸리티 타입 라이브러리입니다.
// 설치: npm install type-fest
import type {
Simplify, // 인터섹션 타입을 단순 객체로 펼침
Merge, // 두 객체 타입 병합 (후자 우선)
PartialDeep, // DeepPartial
ReadonlyDeep, // DeepReadonly
LiteralUnion, // 리터럴 유니온 + 기본 타입
SetOptional, // 특정 키만 optional로
SetRequired, // 특정 키만 required로
Opaque, // Opaque 타입
Promisable, // T | Promise<T>
JsonValue, // JSON 직렬화 가능 타입
CamelCase, // snake_case → camelCase
SnakeCase, // camelCase → snake_case
Get, // 점 표기법 깊은 접근
} from "type-fest";
// Simplify 예시
type Complex = { a: string } & { b: number } & { c: boolean };
type Simple = Simplify<Complex>; // { a: string; b: number; c: boolean }
// LiteralUnion: 자동완성 + 임의 문자열 모두 허용
type FontSize = LiteralUnion<"sm" | "md" | "lg" | "xl", string>;
const size: FontSize = "sm"; // 자동완성
const custom: FontSize = "2rem"; // 임의 값도 OK
// SetOptional: 특정 키만 optional로
interface UserRequired {
id: string;
name: string;
email: string;
age: number;
}
type UserUpdate = SetOptional<UserRequired, "name" | "email" | "age">;
// { id: string; name?: string; email?: string; age?: number }
// Get: 점 표기법으로 깊은 타입 접근
type Config = { server: { host: string; port: number } };
type Host = Get<Config, "server.host">; // string
정리
| 유틸리티 타입 | 구현 핵심 | 주요 용도 |
|---|---|---|
DeepPartial<T> | 재귀 + ? 수정자 | 설정 오버라이드, 부분 업데이트 |
DeepReadonly<T> | 재귀 + readonly 수정자 | 불변 상태, Redux state |
OmitByValue<T, V> | as never 키 필터링 | 직렬화, 함수 제거 |
PickByValue<T, V> | as K 키 필터링 | 타입 기반 선택 |
RequireAtLeastOne<T> | 분산 조건부 타입 + 유니온 | 검색 파라미터, 필터 |
RequireExactlyOne<T> | never 키 배제 | 결제 수단, 인증 방법 |
XOR<T, U> | 상호 배제 + 유니온 | 컨트롤드/언컨트롤드 컴포넌트 |
Brand<T, B> | unique symbol 교차 | 도메인 ID, 단위 타입 |
Opaque<T, Tag> | Symbol 태그 교차 | 해시 비밀번호, 검증된 이메일 |
PartialBy<T, K> | Omit + Partial Pick 조합 | 부분 선택적 필드 |
다음 장에서는 TypeScript의 모듈 시스템과 타입 선언을 다룹니다. .d.ts 파일 작성, 모듈 증강(augmentation), 앰비언트 선언으로 외부 라이브러리를 타입 안전하게 사용하는 방법을 배웁니다.