icon안동민 개발노트

유틸리티 타입 (Partial, Readonly, Pick 등)


 타입스크립트는 일반적인 타입 변환을 쉽게 수행할 수 있도록 여러 유틸리티 타입을 제공합니다.

 이들은 기존 타입을 변형하여 새로운 타입을 생성하는 데 사용됩니다.

주요 유틸리티 타입 개요

  • Partial<T> : 모든 속성을 선택적으로 만듭니다.
  • Readonly<T> : 모든 속성을 읽기 전용으로 만듭니다.
  • Pick<T, K> : 특정 속성만 선택합니다.
  • Omit<T, K> : 특정 속성을 제외합니다.
  • Record<K, T> : 키 타입 K와 값 타입 T를 가진 객체 타입을 생성합니다.
  • Exclude<T, U>: T에서 U에 할당할 수 있는 타입을 제외합니다.
  • Extract<T, U> : T에서 U에 할당할 수 있는 타입을 추출합니다.
  • NonNullable<T> : null과 undefined를 제외합니다.
  • ReturnType<T> : 함수의 반환 타입을 얻습니다.
  • Parameters<T> : 함수의 매개변수 타입을 튜플로 얻습니다.

Partial<T>

 Partial<T>는 모든 속성을 선택적으로 만듭니다.

interface User {
    id: number;
    name: string;
    email: string;
}
 
function updateUser(user: User, fieldsToUpdate: Partial<User>) {
    return { ...user, ...fieldsToUpdate };
}
 
const user: User = {
    id: 1,
    name: "John Doe",
    email: "[email protected]"
};
 
const updatedUser = updateUser(user, { email: "[email protected]" });

 내부 구현

type Partial<T> = {
    [P in keyof T]?: T[P];
};

Readonly<T>

 Readonly<T>는 모든 속성을 읽기 전용으로 만듭니다.

interface Config {
    host: string;
    port: number;
}
 
const config: Readonly<Config> = {
    host: "localhost",
    port: 3000
};
 
// config.port = 8080; // Error: Cannot assign to 'port' because it is a read-only property.

 불변성을 강제하여 예기치 않은 변경을 방지할 수 있습니다.

Pick<T, K>와 Omit<T, K>

 Pick<T, K>는 특정 속성만 선택하고, Omit<T, K>는 특정 속성을 제외합니다.

interface User {
    id: number;
    name: string;
    email: string;
    password: string;
}
 
type PublicUser = Omit<User, "password">;
type LoginCredentials = Pick<User, "email" | "password">;
 
function displayUser(user: PublicUser) {
    console.log(`${user.name} (${user.email})`);
}
 
function login(credentials: LoginCredentials) {
    // 로그인 로직
}

Record<K, T>

 Record<K, T>는 키 타입 K와 값 타입 T를 가진 객체 타입을 생성합니다.

type CityDatabase = Record<string, number>;
 
const populations: CityDatabase = {
    "New York": 8419000,
    "Los Angeles": 3898000,
    "Chicago": 2746000
};
 
function getPopulation(city: string): number {
    return populations[city] || 0;
}

유니온 타입 조작 3종

Exclude<T, U>

Extract<T, U>

NonNullable<T>

 이들은 유니온 타입을 조작하는 데 유용합니다.

type NumberOrString = number | string | null | undefined;
 
type JustNumberOrString = NonNullable<NumberOrString>; // number | string
type JustString = Extract<NumberOrString, string>; // string
type NoString = Exclude<NumberOrString, string>; // number | null | undefined
 
function processValue(value: JustNumberOrString) {
    if (typeof value === "string") {
        return value.toUpperCase();
    } else {
        return value * 2;
    }
}

ReturnType<T>와 Parameters<T>

 이들은 함수 타입을 조작하는 데 사용됩니다.

function greet(name: string, age: number): string {
    return `Hello, ${name}! You are ${age} years old.`;
}
 
type GreetReturn = ReturnType<typeof greet>; // string
type GreetParams = Parameters<typeof greet>; // [string, number]
 
function logged<T extends (...args: any[]) => any>(fn: T) {
    return (...args: Parameters<T>): ReturnType<T> => {
        console.log(`Calling function with args:`, args);
        const result = fn(...args);
        console.log(`Function returned:`, result);
        return result;
    };
}
 
const loggedGreet = logged(greet);
loggedGreet("Alice", 30);

유틸리티 타입 조합

 여러 유틸리티 타입을 조합하여 복잡한 타입 변환을 수행할 수 있습니다.

interface User {
    id: number;
    name: string;
    email: string;
    role: "admin" | "user";
    metadata: Record<string, string>;
}
 
type UpdateableUserFields = Omit<User, "id">;
type OptionalUpdateableUserFields = Partial<UpdateableUserFields>;
 
function updateUser(userId: number, fieldsToUpdate: OptionalUpdateableUserFields) {
    // 업데이트 로직
}

사용자 정의 유틸리티 타입

 프로젝트 특화 유틸리티 타입을 정의할 수 있습니다.

type Mutable<T> = {
    -readonly [P in keyof T]: T[P];
};
 
type DeepPartial<T> = T extends object ? {
    [P in keyof T]?: DeepPartial<T[P]>;
} : T;
 
interface DeepNestedConfig {
    readonly server: {
        readonly host: string;
        readonly port: number;
    };
    readonly database: {
        readonly url: string;
        readonly name: string;
    };
}
 
type MutableDeepPartialConfig = DeepPartial<Mutable<DeepNestedConfig>>;
 
function updateConfig(config: DeepNestedConfig, updates: MutableDeepPartialConfig) {
    // 깊은 업데이트 로직
}

주의사항과 최적화

  1. 성능 : 복잡한 유틸리티 타입은 컴파일 시간을 증가시킬 수 있습니다.
  2. 가독성 : 과도하게 중첩된 유틸리티 타입은 코드 이해를 어렵게 만들 수 있습니다.

 최적화 전략

  • 자주 사용되는 복잡한 타입은 별도의 타입 별칭으로 정의하세요.
  • 타입 추론을 최대한 활용하여 명시적 타입 주석을 줄이세요.
  • 필요한 경우에만 고급 유틸리티 타입을 사용하세요.

설계 원칙과 Best Practices

  1. 단순성 유지 : 가능한 한 간단한 유틸리티 타입 조합을 사용하세요.
  2. 재사용성 고려 : 프로젝트 전반에서 사용될 수 있는 유틸리티 타입은 별도 파일로 분리하세요.
  3. 명확한 이름 사용 : 유틸리티 타입의 목적을 명확히 표현하는 이름을 사용하세요.
  4. 문서화 : 복잡한 유틸리티 타입은 주석으로 설명하세요.
  5. 테스트 작성 : 중요한 유틸리티 타입에 대한 타입 테스트를 작성하세요.
  6. 일관성 유지 : 프로젝트 전체에서 유틸리티 타입 사용 패턴을 일관되게 유지하세요.
  7. IDE 지원 활용 : 유틸리티 타입이 IDE의 자동 완성과 타입 추론을 방해하지 않는지 확인하세요.
  8. 버전 관리 : 타입스크립트 버전 업그레이드 시 유틸리티 타입의 변경사항을 확인하세요.
  9. 실제 사용 사례 기반 : 실제 문제 해결을 위해 유틸리티 타입을 사용하고, 과도한 추상화는 피하세요.
  10. 학습과 공유 : 팀 내에서 유용한 유틸리티 타입 패턴을 공유하고 학습하세요.