TypeScript 'infer' 키워드에 대한 종합 가이드. 조건부 타입과 함께 사용하여 강력한 타입 추출 및 조작 방법을 설명합니다. 고급 사용 사례 포함.
TypeScript infer 마스터하기: 고급 타입 조작을 위한 조건부 타입 추출
TypeScript의 타입 시스템은 개발자가 견고하고 유지보수 가능한 애플리케이션을 구축할 수 있도록 하는 매우 강력한 기능입니다. 이러한 강력함을 가능하게 하는 핵심 기능 중 하나는 조건부 타입과 함께 사용되는 infer
키워드입니다. 이 조합은 복잡한 타입 구조에서 특정 타입을 추출하는 메커니즘을 제공합니다. 이 블로그 게시물에서는 infer
키워드를 깊이 파고들어 기능과 고급 사용 사례를 설명합니다. API 상호 작용부터 복잡한 데이터 구조 조작에 이르기까지 다양한 소프트웨어 개발 시나리오에 적용 가능한 실용적인 예제를 탐구할 것입니다.
조건부 타입이란 무엇인가요?
infer
에 대해 자세히 알아보기 전에 조건부 타입을 간단히 복습해 보겠습니다. TypeScript의 조건부 타입은 JavaScript의 삼항 연산자와 유사하게 조건에 따라 타입을 정의할 수 있도록 합니다. 기본 구문은 다음과 같습니다.
T extends U ? X : Y
이것은 "만약 타입 T
가 타입 U
에 할당 가능하다면, 타입은 X
이고, 그렇지 않으면 타입은 Y
입니다."라고 읽습니다.
예시:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
infer
키워드 소개
infer
키워드는 조건부 타입의 extends
절 내에서 사용되어 검사되는 타입으로부터 추론될 수 있는 타입 변수를 선언합니다. 본질적으로는 나중에 사용하기 위해 타입의 일부를 "캡처"할 수 있게 합니다.
기본 구문:
type MyType<T> = T extends (infer U) ? U : never;
이 예시에서 T
가 어떤 타입에 할당 가능하다면, TypeScript는 U
의 타입을 추론하려고 시도할 것입니다. 추론이 성공하면 타입은 U
가 되고, 그렇지 않으면 never
가 됩니다.
infer
의 간단한 예시
1. 함수의 반환 타입 추론
일반적인 사용 사례는 함수의 반환 타입을 추론하는 것입니다.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
이 예시에서 ReturnType<T>
는 함수 타입 T
를 입력으로 받습니다. T
가 어떤 인수를 받든 값을 반환하는 함수에 할당 가능한지 확인합니다. 그렇다면 반환 타입을 R
로 추론하여 반환합니다. 그렇지 않으면 any
를 반환합니다.
2. 배열 요소 타입 추론
또 다른 유용한 시나리오는 배열에서 요소 타입을 추출하는 것입니다.
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
여기서 ArrayElementType<T>
는 T
가 배열 타입인지 확인합니다. 배열이라면 요소 타입을 U
로 추론하여 반환합니다. 그렇지 않으면 never
를 반환합니다.
infer
의 고급 사용 사례
1. 생성자 매개변수 추론
infer
를 사용하여 생성자 함수의 매개변수 타입을 추출할 수 있습니다.
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
이 경우 ConstructorParameters<T>
는 생성자 함수 타입 T
를 받습니다. 생성자 매개변수의 타입을 P
로 추론하여 튜플로 반환합니다.
2. 객체 타입에서 속성 추출
infer
는 매핑된 타입과 조건부 타입을 사용하여 객체 타입에서 특정 속성을 추출하는 데에도 사용할 수 있습니다.
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//지리 좌표를 나타내는 인터페이스.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
여기서 PickByType<T, K, U>
는 T
의 속성(K
에 키가 있는) 중 값이 타입 U
에 할당 가능한 것들만 포함하는 새 타입을 생성합니다. 매핑된 타입은 T
의 키를 반복하고, 조건부 타입은 지정된 타입과 일치하지 않는 키를 필터링합니다.
3. Promise와 함께 작업하기
Promise
의 해결된 타입을 추론할 수 있습니다.
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Awaited<T>
타입은 Promise
일 것으로 예상되는 타입 T
를 받습니다. 그런 다음 Promise
의 해결된 타입 U
를 추론하여 반환합니다. T
가 promise가 아니면 T를 반환합니다. 이것은 최신 버전의 TypeScript에 내장된 유틸리티 타입입니다.
4. Promise 배열 타입 추출
Awaited
와 배열 타입 추론을 결합하면 Promise 배열에 의해 해결된 타입을 추론할 수 있습니다. Promise.all
을 다룰 때 특히 유용합니다.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
이 예시는 먼저 두 개의 비동기 함수인 getUSDRate
와 getEURRate
를 정의하여 환율을 가져오는 것을 시뮬레이션합니다. 그런 다음 PromiseArrayReturnType
유틸리티 타입은 배열의 각 Promise
에서 해결된 타입을 추출하여 해당 Promise의 해당 타입의 awaited 타입인 각 요소를 가진 튜플 타입이 됩니다.
다양한 도메인에 걸친 실용적인 예시
1. 전자상거래 애플리케이션
API에서 제품 세부 정보를 가져오는 전자상거래 애플리케이션을 고려해 보세요. infer
를 사용하여 제품 데이터의 타입을 추출할 수 있습니다.
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// API 호출 시뮬레이션
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'A sample product',
imageUrl: 'https://example.com/image.jpg',
category: 'Electronics',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Product Name: ${product.name}`);
console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
이 예시에서는 Product
인터페이스와 API에서 제품 세부 정보를 가져오는 fetchProduct
함수를 정의합니다. Awaited
와 ReturnType
을 사용하여 fetchProduct
함수의 반환 타입에서 Product
타입을 추출하여 displayProductDetails
함수를 타입 검사할 수 있도록 합니다.
2. 국제화 (i18n)
로케일에 따라 다른 문자열을 반환하는 번역 함수가 있다고 가정해 보겠습니다. infer
를 사용하여 이 함수의 반환 타입을 추출하여 타입 안전성을 확보할 수 있습니다.
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
여기서 TranslationType
은 Translations
인터페이스로 추론되어 greetUser
함수가 번역된 문자열에 액세스하기 위한 올바른 타입 정보를 갖도록 합니다.
3. API 응답 처리
API와 작업할 때 응답 구조는 복잡할 수 있습니다. infer
는 중첩된 API 응답에서 특정 데이터 타입을 추출하는 데 도움이 될 수 있습니다.
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// API 호출 시뮬레이션
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Name: ${profile.firstName} ${profile.lastName}`);
console.log(`Country: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
이 예시에서는 ApiResponse
인터페이스와 UserData
인터페이스를 정의합니다. infer
와 타입 인덱싱을 사용하여 API 응답에서 UserProfileType
을 추출하여 displayUserProfile
함수가 올바른 타입을 받도록 합니다.
infer
사용 모범 사례
- 단순하게 유지:
infer
는 필요할 때만 사용하십시오. 과도한 사용은 코드를 읽고 이해하기 어렵게 만들 수 있습니다. - 타입 문서화: 조건부 타입 및
infer
문이 무엇을 하고 있는지 설명하는 주석을 추가하십시오. - 타입 테스트: TypeScript의 타입 검사를 사용하여 타입이 예상대로 작동하는지 확인하십시오.
- 성능 고려: 복잡한 조건부 타입은 때때로 컴파일 시간에 영향을 미칠 수 있습니다. 타입의 복잡성을 염두에 두십시오.
- 유틸리티 타입 사용: TypeScript는 사용자 지정
infer
문 없이도 많은 일반적인 타입 조작 작업을 처리할 수 있는 여러 기본 유틸리티 타입을 제공합니다(예:ReturnType
,Awaited
).
일반적인 함정
- 잘못된 추론: 때때로 TypeScript는 예상과 다른 타입을 추론할 수 있습니다. 타입 정의 및 조건을 다시 확인하십시오.
- 순환 종속성:
infer
를 사용한 재귀 타입 정의 시 주의하십시오. 순환 종속성 및 컴파일 오류로 이어질 수 있습니다. - 지나치게 복잡한 타입: 이해하고 유지 관리하기 어려운 지나치게 복잡한 조건부 타입을 피하십시오. 더 작고 관리하기 쉬운 타입으로 분해하십시오.
infer
의 대안
infer
는 강력한 도구이지만 대안적인 접근 방식이 더 적합한 경우가 있습니다.
- 타입 단언: 경우에 따라 타입 단언을 사용하여 타입을 추론하는 대신 값의 타입을 명시적으로 지정할 수 있습니다. 그러나 타입 단언은 타입 검사를 우회할 수 있으므로 주의하십시오.
- 타입 가드: 타입 가드는 런타임 검사를 기반으로 값의 타입을 좁히는 데 사용할 수 있습니다. 이는 런타임 조건에 따라 다른 타입을 처리해야 할 때 유용합니다.
- 유틸리티 타입: TypeScript는 사용자 지정
infer
문 없이도 많은 일반적인 타입 조작 작업을 처리할 수 있는 풍부한 유틸리티 타입을 제공합니다.
결론
TypeScript의 infer
키워드는 조건부 타입과 결합될 때 고급 타입 조작 기능을 활용합니다. 이를 통해 복잡한 타입 구조에서 특정 타입을 추출하여 더 견고하고 유지보수 가능하며 타입 안전한 코드를 작성할 수 있습니다. 함수 반환 타입 추론부터 객체 타입 속성 추출에 이르기까지 가능성은 무궁무진합니다. 이 가이드에 설명된 원칙과 모범 사례를 이해함으로써 infer
를 최대한 활용하고 TypeScript 기술을 향상시킬 수 있습니다. 타입 문서를 작성하고 철저히 테스트하며 적절한 경우 대안적인 접근 방식을 고려하십시오. infer
마스터는 진정으로 표현력 있고 강력한 TypeScript 코드를 작성할 수 있도록 하여 궁극적으로 더 나은 소프트웨어로 이어집니다.