한국어

TypeScript의 유틸리티 타입을 마스터하세요. 타입 변환, 코드 재사용성 향상, 애플리케이션의 타입 안정성 강화를 위한 강력한 도구입니다.

TypeScript 유틸리티 타입: 내장 타입 조작 도구

TypeScript는 JavaScript에 정적 타이핑을 도입한 강력한 언어입니다. 그 핵심 기능 중 하나는 타입을 조작하는 능력으로, 개발자가 더 견고하고 유지보수하기 쉬운 코드를 작성할 수 있게 해줍니다. TypeScript는 일반적인 타입 변환을 단순화하는 내장 유틸리티 타입 세트를 제공합니다. 이러한 유틸리티 타입은 타입 안정성을 강화하고, 코드 재사용성을 높이며, 개발 워크플로우를 간소화하는 데 매우 유용한 도구입니다. 이 종합 가이드에서는 가장 필수적인 TypeScript 유틸리티 타입을 탐색하고, 이를 마스터하는 데 도움이 되는 실용적인 예제와 실행 가능한 통찰력을 제공합니다.

TypeScript 유틸리티 타입이란 무엇인가요?

유틸리티 타입은 기존 타입을 새로운 타입으로 변환하는 미리 정의된 타입 연산자입니다. 이는 TypeScript 언어에 내장되어 있으며, 일반적인 타입 조작을 간결하고 선언적으로 수행하는 방법을 제공합니다. 유틸리티 타입을 사용하면 상용구 코드를 크게 줄이고 타입 정의를 더 표현력 있고 이해하기 쉽게 만들 수 있습니다.

값이 아닌 타입에 대해 작동하는 함수라고 생각하면 됩니다. 이들은 타입을 입력으로 받아 수정된 타입을 출력으로 반환합니다. 이를 통해 최소한의 코드로 복잡한 타입 관계와 변환을 생성할 수 있습니다.

유틸리티 타입을 사용하는 이유는 무엇인가요?

TypeScript 프로젝트에 유틸리티 타입을 통합해야 하는 몇 가지 강력한 이유가 있습니다:

필수 TypeScript 유틸리티 타입

TypeScript에서 가장 일반적으로 사용되고 유용한 유틸리티 타입 몇 가지를 살펴보겠습니다. 각 타입의 목적, 구문, 그리고 사용법을 설명하기 위한 실용적인 예제를 다룰 것입니다.

1. Partial<T>

Partial<T> 유틸리티 타입은 타입 T의 모든 속성을 선택적으로 만듭니다. 이는 기존 타입의 일부 또는 모든 속성을 가지는 새로운 타입을 만들고 싶지만, 모든 속성이 존재하도록 강제하고 싶지 않을 때 유용합니다.

구문:

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

예제:

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

type OptionalUser = Partial<User>; // 모든 속성이 이제 선택 사항입니다

const partialUser: OptionalUser = {
 name: "Alice", // name 속성만 제공
};

사용 사례: 특정 속성만으로 객체를 업데이트할 때. 예를 들어, 사용자 프로필 업데이트 양식을 상상해보세요. 사용자가 한 번에 모든 필드를 업데이트하도록 요구하고 싶지 않을 것입니다.

2. Required<T>

Required<T> 유틸리티 타입은 타입 T의 모든 속성을 필수로 만듭니다. 이것은 Partial<T>의 반대입니다. 선택적 속성을 가진 타입이 있을 때 모든 속성이 존재하도록 보장하고 싶을 때 유용합니다.

구문:

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

예제:

interface Config {
 apiKey?: string;
 apiUrl?: string;
}

type CompleteConfig = Required<Config>; // 모든 속성이 이제 필수입니다

const config: CompleteConfig = {
 apiKey: "your-api-key",
 apiUrl: "https://example.com/api",
};

사용 사례: 애플리케이션을 시작하기 전에 모든 구성 설정이 제공되었는지 강제할 때. 이는 누락되거나 정의되지 않은 설정으로 인한 런타임 오류를 방지하는 데 도움이 될 수 있습니다.

3. Readonly<T>

Readonly<T> 유틸리티 타입은 타입 T의 모든 속성을 읽기 전용으로 만듭니다. 이는 객체가 생성된 후 실수로 속성을 수정하는 것을 방지합니다. 이는 불변성을 촉진하고 코드의 예측 가능성을 향상시킵니다.

구문:

type Readonly<T> = { readonly [P in keyof T]: T[P]; };

예제:

interface Product {
 id: number;
 name: string;
 price: number;
}

type ImmutableProduct = Readonly<Product>; // 모든 속성이 이제 읽기 전용입니다

const product: ImmutableProduct = {
 id: 123,
 name: "Example Product",
 price: 25.99,
};

// product.price = 29.99; // 오류: 'price'는 읽기 전용 속성이므로 할당할 수 없습니다.

사용 사례: 생성 후 수정되어서는 안 되는 구성 객체나 데이터 전송 객체(DTO)와 같은 불변 데이터 구조를 만들 때. 이는 특히 함수형 프로그래밍 패러다임에서 유용합니다.

4. Pick<T, K extends keyof T>

Pick<T, K extends keyof T> 유틸리티 타입은 타입 T에서 속성 집합 K를 선택하여 새로운 타입을 만듭니다. 이는 기존 타입의 속성 중 일부만 필요할 때 유용합니다.

구문:

type Pick<T, K extends keyof T> = { [P in K]: T[P]; };

예제:

interface Employee {
 id: number;
 name: string;
 department: string;
salary: number;
}

type EmployeeNameAndDepartment = Pick<Employee, "name" | "department">; // name과 department만 선택

const employeeInfo: EmployeeNameAndDepartment = {
 name: "Bob",
 department: "Engineering",
};

사용 사례: 특정 작업에 필요한 데이터만 포함하는 특수 데이터 전송 객체(DTO)를 만들 때. 이는 성능을 향상시키고 네트워크를 통해 전송되는 데이터의 양을 줄일 수 있습니다. 클라이언트에 사용자 세부 정보를 보내지만 급여와 같은 민감한 정보는 제외하는 경우를 상상해보세요. Pick을 사용하여 `id`와 `name`만 보낼 수 있습니다.

5. Omit<T, K extends keyof any>

Omit<T, K extends keyof any> 유틸리티 타입은 타입 T에서 속성 집합 K를 생략하여 새로운 타입을 만듭니다. 이는 Pick<T, K extends keyof T>의 반대이며, 기존 타입에서 특정 속성을 제외하고 싶을 때 유용합니다.

구문:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

예제:

interface Event {
 id: number;
 title: string;
description: string;
 date: Date;
 location: string;
}

type EventSummary = Omit<Event, "description" | "location">; // description과 location 생략

const eventPreview: EventSummary = {
 id: 1,
 title: "Conference",
 date: new Date(),
};

사용 사례: 전체 설명과 위치를 포함하지 않고 이벤트 요약을 표시하는 등 특정 목적을 위해 데이터 모델의 단순화된 버전을 만들 때. 이는 또한 클라이언트에 데이터를 보내기 전에 민감한 필드를 제거하는 데 사용될 수 있습니다.

6. Exclude<T, U>

Exclude<T, U> 유틸리티 타입은 T에서 U에 할당 가능한 모든 타입을 제외하여 새로운 타입을 만듭니다. 이는 유니언 타입에서 특정 타입을 제거하고 싶을 때 유용합니다.

구문:

type Exclude<T, U> = T extends U ? never : T;

예제:

type AllowedFileTypes = "image" | "video" | "audio" | "document";
type MediaFileTypes = "image" | "video" | "audio";

type DocumentFileTypes = Exclude<AllowedFileTypes, MediaFileTypes>; // "document"

const fileType: DocumentFileTypes = "document";

사용 사례: 특정 컨텍스트에서 관련 없는 특정 타입을 제거하기 위해 유니언 타입을 필터링할 때. 예를 들어, 허용된 파일 타입 목록에서 특정 파일 타입을 제외하고 싶을 수 있습니다.

7. Extract<T, U>

Extract<T, U> 유틸리티 타입은 T에서 U에 할당 가능한 모든 타입을 추출하여 새로운 타입을 만듭니다. 이는 Exclude<T, U>의 반대이며, 유니언 타입에서 특정 타입을 선택하고 싶을 때 유용합니다.

구문:

type Extract<T, U> = T extends U ? T : never;

예제:

type InputTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = string | number | boolean;

type NonNullablePrimitives = Extract<InputTypes, PrimitiveTypes>; // string | number | boolean

const value: NonNullablePrimitives = "hello";

사용 사례: 특정 기준에 따라 유니언 타입에서 특정 타입을 선택할 때. 예를 들어, 원시 타입과 객체 타입을 모두 포함하는 유니언 타입에서 모든 원시 타입을 추출하고 싶을 수 있습니다.

8. NonNullable<T>

NonNullable<T> 유틸리티 타입은 타입 T에서 nullundefined를 제외하여 새로운 타입을 만듭니다. 이는 타입이 null 또는 undefined가 될 수 없음을 보장하고 싶을 때 유용합니다.

구문:

type NonNullable<T> = T extends null | undefined ? never : T;

예제:

type MaybeString = string | null | undefined;

type DefinitelyString = NonNullable<MaybeString>; // string

const message: DefinitelyString = "Hello, world!";

사용 사례: 값에 대한 작업을 수행하기 전에 해당 값이 null 또는 undefined가 아님을 강제할 때. 이는 예기치 않은 null 또는 undefined 값으로 인한 런타임 오류를 방지하는 데 도움이 될 수 있습니다. 사용자의 주소를 처리해야 하고 어떤 작업을 하기 전에 주소가 null이 아니어야 하는 시나리오를 생각해보세요.

9. ReturnType<T extends (...args: any) => any>

ReturnType<T extends (...args: any) => any> 유틸리티 타입은 함수 타입 T의 반환 타입을 추출합니다. 이는 함수가 반환하는 값의 타입을 알고 싶을 때 유용합니다.

구문:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

예제:

function fetchData(url: string): Promise<{ data: any }> {
 return fetch(url).then(response => response.json());
}

type FetchDataReturnType = ReturnType<typeof fetchData>; // Promise<{ data: any }>

async function processData(data: FetchDataReturnType) {
 // ...
}

사용 사례: 함수가 반환하는 값의 타입을 결정할 때, 특히 비동기 작업이나 복잡한 함수 시그니처를 다룰 때 유용합니다. 이를 통해 반환된 값을 올바르게 처리하고 있는지 확인할 수 있습니다.

10. Parameters<T extends (...args: any) => any>

Parameters<T extends (...args: any) => any> 유틸리티 타입은 함수 타입 T의 매개변수 타입을 튜플로 추출합니다. 이는 함수가 받는 인수의 타입을 알고 싶을 때 유용합니다.

구문:

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

예제:

function createUser(name: string, age: number, email: string): void {
 // ...
}

type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]

function logUser(...args: CreateUserParams) {
 console.log("Creating user with:", args);
}

사용 사례: 함수가 받는 인수의 타입을 결정하는 데 유용하며, 이는 다양한 시그니처를 가진 함수와 함께 작동해야 하는 제네릭 함수나 데코레이터를 만들 때 유용할 수 있습니다. 함수에 동적으로 인수를 전달할 때 타입 안전성을 보장하는 데 도움이 됩니다.

11. ConstructorParameters<T extends abstract new (...args: any) => any>

ConstructorParameters<T extends abstract new (...args: any) => any> 유틸리티 타입은 생성자 함수 타입 T의 매개변수 타입을 튜플로 추출합니다. 이는 생성자가 받는 인수의 타입을 알고 싶을 때 유용합니다.

구문:

type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

예제:

class Logger {
 constructor(public prefix: string, public enabled: boolean) {}
 log(message: string) {
 if (this.enabled) {
 console.log(`${this.prefix}: ${message}`);
 }
 }
}

type LoggerConstructorParams = ConstructorParameters<typeof Logger>; // [string, boolean]

function createLogger(...args: LoggerConstructorParams) {
 return new Logger(...args);
}

사용 사례: Parameters와 유사하지만 생성자 함수에 특화되어 있습니다. 다양한 생성자 시그니처를 가진 클래스를 동적으로 인스턴스화해야 하는 팩토리나 의존성 주입 시스템을 만들 때 도움이 됩니다.

12. InstanceType<T extends abstract new (...args: any) => any>

InstanceType<T extends abstract new (...args: any) => any> 유틸리티 타입은 생성자 함수 타입 T의 인스턴스 타입을 추출합니다. 이는 생성자가 만드는 객체의 타입을 알고 싶을 때 유용합니다.

구문:

type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

예제:

class Greeter {
 greeting: string;
 constructor(message: string) {
 this.greeting = message;
 }
 greet() {
 return "Hello, " + this.greeting;
 }
}

type GreeterInstance = InstanceType<typeof Greeter>; // Greeter

const myGreeter: GreeterInstance = new Greeter("World");
console.log(myGreeter.greet());

사용 사례: 생성자에 의해 생성된 객체의 타입을 결정하는 데 유용하며, 상속이나 다형성으로 작업할 때 유용합니다. 이는 클래스의 인스턴스를 타입 안전하게 참조하는 방법을 제공합니다.

13. Record<K extends keyof any, T>

Record<K extends keyof any, T> 유틸리티 타입은 속성 키가 K이고 속성 값이 T인 객체 타입을 구성합니다. 이는 미리 키를 알고 있는 딕셔너리 같은 타입을 만드는 데 유용합니다.

구문:

type Record<K extends keyof any, T> = { [P in K]: T; };

예제:

type CountryCode = "US" | "CA" | "GB" | "DE";

type CurrencyMap = Record<CountryCode, string>; // { US: string; CA: string; GB: string; DE: string; }

const currencies: CurrencyMap = {
 US: "USD",
 CA: "CAD",
 GB: "GBP",
 DE: "EUR",
};

사용 사례: 고정된 키 집합이 있고 모든 키가 특정 타입의 값을 가지도록 보장하고 싶을 때 딕셔너리 같은 객체를 만드는 데 사용됩니다. 이는 구성 파일, 데이터 매핑 또는 조회 테이블로 작업할 때 일반적입니다.

사용자 정의 유틸리티 타입

TypeScript의 내장 유틸리티 타입은 강력하지만, 프로젝트의 특정 요구 사항을 해결하기 위해 자신만의 사용자 정의 유틸리티 타입을 만들 수도 있습니다. 이를 통해 복잡한 타입 변환을 캡슐화하고 코드베이스 전체에서 재사용할 수 있습니다.

예제:

// 특정 타입을 가진 객체의 키를 얻는 유틸리티 타입
type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];

interface Person {
 name: string;
 age: number;
 address: string;
 phoneNumber: number;
}

type StringKeys = KeysOfType<Person, string>; // "name" | "address"

유틸리티 타입 사용을 위한 모범 사례

결론

TypeScript 유틸리티 타입은 코드의 타입 안정성, 재사용성, 유지보수성을 크게 향상시킬 수 있는 강력한 도구입니다. 이러한 유틸리티 타입을 마스터함으로써 더 견고하고 표현력 있는 TypeScript 애플리케이션을 작성할 수 있습니다. 이 가이드는 가장 필수적인 TypeScript 유틸리티 타입을 다루었으며, 프로젝트에 이를 통합하는 데 도움이 되는 실용적인 예제와 실행 가능한 통찰력을 제공했습니다.

이러한 유틸리티 타입을 실험하고 자신의 코드에서 특정 문제를 해결하는 데 어떻게 사용될 수 있는지 탐색하는 것을 잊지 마세요. 익숙해질수록 더 깨끗하고 유지보수하기 쉬우며 타입 안전한 TypeScript 애플리케이션을 만들기 위해 점점 더 많이 사용하게 될 것입니다. 웹 애플리케이션, 서버 측 애플리케이션 또는 그 어떤 것을 구축하든, 유틸리티 타입은 개발 워크플로우와 코드 품질을 향상시키는 데 유용한 도구 세트를 제공합니다. 이러한 내장 타입 조작 도구를 활용하여 TypeScript의 잠재력을 최대한 발휘하고 표현력이 뛰어나면서도 견고한 코드를 작성할 수 있습니다.