한국어

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]

이 예시는 먼저 두 개의 비동기 함수인 getUSDRategetEURRate를 정의하여 환율을 가져오는 것을 시뮬레이션합니다. 그런 다음 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 함수를 정의합니다. AwaitedReturnType을 사용하여 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!

여기서 TranslationTypeTranslations 인터페이스로 추론되어 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의 infer 키워드는 조건부 타입과 결합될 때 고급 타입 조작 기능을 활용합니다. 이를 통해 복잡한 타입 구조에서 특정 타입을 추출하여 더 견고하고 유지보수 가능하며 타입 안전한 코드를 작성할 수 있습니다. 함수 반환 타입 추론부터 객체 타입 속성 추출에 이르기까지 가능성은 무궁무진합니다. 이 가이드에 설명된 원칙과 모범 사례를 이해함으로써 infer를 최대한 활용하고 TypeScript 기술을 향상시킬 수 있습니다. 타입 문서를 작성하고 철저히 테스트하며 적절한 경우 대안적인 접근 방식을 고려하십시오. infer 마스터는 진정으로 표현력 있고 강력한 TypeScript 코드를 작성할 수 있도록 하여 궁극적으로 더 나은 소프트웨어로 이어집니다.