한국어

TypeScript 조건부 타입의 강력한 기능을 활용하여 강력하고 유연하며 유지 관리 가능한 API를 구축하세요. 전 세계 소프트웨어 프로젝트를 위해 타입 추론을 활용하고 적응형 인터페이스를 만드세요.

고급 API 설계를 위한 TypeScript 조건부 타입

소프트웨어 개발 세계에서 API(Application Programming Interfaces)를 구축하는 것은 기본적인 관행입니다. 잘 설계된 API는 특히 글로벌 사용자 기반을 다룰 때 모든 애플리케이션의 성공에 매우 중요합니다. 강력한 타입 시스템을 갖춘 TypeScript는 개발자에게 기능적일 뿐만 아니라 강력하고, 유지 관리 가능하며, 이해하기 쉬운 API를 만들 수 있는 도구를 제공합니다. 이러한 도구 중에서 조건부 타입은 고급 API 설계를 위한 핵심 요소로 두드러집니다. 이 블로그 게시물에서는 조건부 타입의 복잡성을 살펴보고 이를 활용하여 더욱 적응성이 뛰어나고 타입 안전한 API를 구축하는 방법을 보여줍니다.

조건부 타입 이해

핵심적으로 TypeScript의 조건부 타입을 사용하면 다른 값의 타입에 따라 모양이 달라지는 타입을 만들 수 있습니다. 코드에서 `if...else` 문을 사용하는 방식과 유사하게, 타입 수준의 논리를 도입합니다. 이 조건부 논리는 값의 타입이 다른 값 또는 매개변수의 특성에 따라 달라져야 하는 복잡한 시나리오를 처리할 때 특히 유용합니다. 구문은 매우 직관적입니다.


type ResultType = T extends string ? string : number;

이 예에서 `ResultType`은 조건부 타입입니다. 제네릭 타입 `T`가 `string`을 확장(할당 가능)하는 경우 결과 타입은 `string`이고, 그렇지 않은 경우 `number`입니다. 이 간단한 예는 핵심 개념을 보여줍니다. 입력 타입에 따라 다른 출력 타입을 얻습니다.

기본 구문 및 예시

구문을 더 자세히 살펴보겠습니다.

이해를 돕기 위해 몇 가지 더 많은 예시가 있습니다.


type StringOrNumber = T extends string ? string : number;

let a: StringOrNumber = 'hello'; // string
let b: StringOrNumber = 123; // number

이 경우 입력 타입 `T`에 따라 `string` 또는 `number`가 되는 `StringOrNumber` 타입을 정의합니다. 이 간단한 예는 다른 타입의 속성을 기반으로 타입을 정의하는 조건부 타입의 강력함을 보여줍니다.


type Flatten = T extends (infer U)[] ? U : T;

let arr1: Flatten = 'hello'; // string
let arr2: Flatten = 123; // number

이 `Flatten` 타입은 배열에서 요소 타입을 추출합니다. 이 예에서는 조건 내에서 타입을 정의하는 데 사용되는 `infer`를 사용합니다. `infer U`는 배열에서 타입 `U`를 추론하며, `T`가 배열인 경우 결과 타입은 `U`입니다.

API 설계의 고급 애플리케이션

조건부 타입은 유연하고 타입 안전한 API를 만드는 데 매우 중요합니다. 다양한 기준에 따라 적응하는 타입을 정의할 수 있습니다. 몇 가지 실용적인 애플리케이션은 다음과 같습니다.

1. 동적 응답 타입 생성

요청 매개변수에 따라 다른 데이터를 반환하는 가상 API를 생각해 보세요. 조건부 타입을 사용하면 응답 타입을 동적으로 모델링할 수 있습니다.


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

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

type ApiResponse = 
  T extends 'user' ? User : Product;

function fetchData(type: T): ApiResponse {
  if (type === 'user') {
    return { id: 1, name: 'John Doe', email: 'john.doe@example.com' } as ApiResponse; // TypeScript knows this is a User
  } else {
    return { id: 1, name: 'Widget', price: 19.99 } as ApiResponse; // TypeScript knows this is a Product
  }
}

const userData = fetchData('user'); // userData is of type User
const productData = fetchData('product'); // productData is of type Product

이 예에서 `ApiResponse` 타입은 입력 매개변수 `T`에 따라 동적으로 변경됩니다. TypeScript가 `type` 매개변수를 기반으로 반환된 데이터의 정확한 구조를 알기 때문에 이는 타입 안전성을 향상시킵니다. 이렇게 하면 유니온 타입과 같은 타입 안전성이 떨어질 수 있는 대안을 사용할 필요가 없습니다.

2. 타입 안전한 오류 처리 구현

API는 요청 성공 여부에 따라 다른 응답 모양을 반환하는 경우가 많습니다. 조건부 타입은 이러한 시나리오를 우아하게 모델링할 수 있습니다.


interface SuccessResponse {
  status: 'success';
  data: T;
}

interface ErrorResponse {
  status: 'error';
  message: string;
}

type ApiResult = T extends any ? SuccessResponse | ErrorResponse : never;

function processData(data: T, success: boolean): ApiResult {
  if (success) {
    return { status: 'success', data } as ApiResult;
  } else {
    return { status: 'error', message: 'An error occurred' } as ApiResult;
  }
}

const result1 = processData({ name: 'Test', value: 123 }, true); // SuccessResponse<{ name: string; value: number; }>
const result2 = processData({ name: 'Test', value: 123 }, false); // ErrorResponse

여기서 `ApiResult`는 `SuccessResponse` 또는 `ErrorResponse`가 될 수 있는 API 응답의 구조를 정의합니다. `processData` 함수는 `success` 매개변수에 따라 올바른 응답 타입이 반환되도록 합니다.

3. 유연한 함수 오버로드 생성

조건부 타입은 함수 오버로드와 함께 사용하여 고도로 적응 가능한 API를 만들 수도 있습니다. 함수 오버로드를 사용하면 함수가 매개변수 타입과 반환 타입이 다른 여러 서명을 가질 수 있습니다. 다양한 소스에서 데이터를 가져올 수 있는 API를 생각해 보세요.


function fetchDataOverload(resource: T): Promise;
function fetchDataOverload(resource: string): Promise;

async function fetchDataOverload(resource: string): Promise {
    if (resource === 'users') {
        // Simulate fetching users from an API
        return new Promise((resolve) => {
            setTimeout(() => resolve([{ id: 1, name: 'User 1', email: 'user1@example.com' }]), 100);
        });
    } else if (resource === 'products') {
        // Simulate fetching products from an API
        return new Promise((resolve) => {
            setTimeout(() => resolve([{ id: 1, name: 'Product 1', price: 10.00 }]), 100);
        });
    } else {
        // Handle other resources or errors
        return new Promise((resolve) => {
            setTimeout(() => resolve([]), 100);
        });
    }
}

(async () => {
    const users = await fetchDataOverload('users'); // users is of type User[]
    const products = await fetchDataOverload('products'); // products is of type Product[]
    console.log(users[0].name); // Access user properties safely
    console.log(products[0].name); // Access product properties safely
})();

여기서 첫 번째 오버로드는 `resource`가 'users'인 경우 반환 타입이 `User[]`임을 지정합니다. 두 번째 오버로드는 리소스가 'products'인 경우 반환 타입이 `Product[]`임을 지정합니다. 이 설정을 통해 함수에 제공된 입력에 따라 보다 정확한 타입 검사가 가능하며, 이는 코드 완성 및 오류 감지를 개선합니다.

4. 유틸리티 타입 생성

조건부 타입은 기존 타입을 변환하는 유틸리티 타입을 구축하기 위한 강력한 도구입니다. 이러한 유틸리티 타입은 데이터 구조를 조작하고 API에서 더 재사용 가능한 구성 요소를 만드는 데 유용할 수 있습니다.


interface Person {
  name: string;
  age: number;
  address: {
    street: string;
    city: string;
    country: string;
  };
}

type DeepReadonly = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K];
};

const readonlyPerson: DeepReadonly = {
  name: 'John',
  age: 30,
  address: {
    street: '123 Main St',
    city: 'Anytown',
    country: 'USA',
  },
};

// readonlyPerson.name = 'Jane'; // Error: Cannot assign to 'name' because it is a read-only property.
// readonlyPerson.address.street = '456 Oak Ave'; // Error: Cannot assign to 'street' because it is a read-only property.

이 `DeepReadonly` 타입은 객체와 중첩된 객체의 모든 속성을 읽기 전용으로 만듭니다. 이 예는 조건부 타입을 재귀적으로 사용하여 복잡한 타입 변환을 만드는 방법을 보여줍니다. 이는 불변 데이터가 선호되는 시나리오, 특히 동시 프로그래밍이나 여러 모듈 간에 데이터를 공유할 때 매우 중요하며 추가적인 안전성을 제공합니다.

5. API 응답 데이터 추상화

실제 API 상호 작용에서 래핑된 응답 구조로 작업하는 경우가 많습니다. 조건부 타입은 다양한 응답 래퍼를 처리하는 것을 간소화할 수 있습니다.


interface ApiResponseWrapper {
  data: T;
  meta: {
    total: number;
    page: number;
  };
}

type UnwrapApiResponse = T extends ApiResponseWrapper ? U : T;

function processApiResponse(response: ApiResponseWrapper): UnwrapApiResponse {
  return response.data;
}

interface ProductApiData {
  name: string;
  price: number;
}

const productResponse: ApiResponseWrapper = {
  data: {
    name: 'Example Product',
    price: 20,
  },
  meta: {
    total: 1,
    page: 1,
  },
};

const unwrappedProduct = processApiResponse(productResponse); // unwrappedProduct is of type ProductApiData

이 예에서 `UnwrapApiResponse`는 `ApiResponseWrapper`에서 내부 `data` 타입을 추출합니다. 이를 통해 API 소비자는 항상 래퍼를 처리하지 않고도 핵심 데이터 구조로 작업할 수 있습니다. 이는 API 응답을 일관되게 조정하는 데 매우 유용합니다.

조건부 타입 사용을 위한 모범 사례

조건부 타입은 강력하지만 부적절하게 사용하면 코드가 더 복잡해질 수도 있습니다. 조건부 타입을 효과적으로 활용하기 위한 몇 가지 모범 사례는 다음과 같습니다.

실제 예시 및 글로벌 고려 사항

특히 글로벌 대상을 위한 API를 설계할 때 조건부 타입이 빛을 발하는 실제 시나리오를 살펴보겠습니다.

이러한 예는 글로벌화를 효과적으로 관리하고 국제적인 청중의 다양한 요구 사항을 충족하는 API를 만드는 데 있어 조건부 타입의 다재다능함을 강조합니다. 글로벌 청중을 위한 API를 구축할 때 시간대, 통화, 날짜 형식 및 언어 설정을 고려하는 것이 중요합니다. 조건부 타입을 사용함으로써 개발자는 위치에 관계없이 뛰어난 사용자 경험을 제공하는 적응형 타입 안전 API를 만들 수 있습니다.

함정 및 이를 방지하는 방법

조건부 타입은 매우 유용하지만 피해야 할 잠재적인 함정이 있습니다.

결론

TypeScript 조건부 타입은 고급 API를 설계하기 위한 강력한 메커니즘을 제공합니다. 이를 통해 개발자는 유연하고 타입 안전하며 유지 관리 가능한 코드를 만들 수 있습니다. 조건부 타입을 마스터함으로써 프로젝트의 변화하는 요구 사항에 쉽게 적응하는 API를 구축하여 글로벌 소프트웨어 개발 환경에서 강력하고 확장 가능한 애플리케이션을 구축하기 위한 초석으로 만들 수 있습니다. 조건부 타입의 강력한 기능을 활용하고 API 디자인의 품질과 유지 관리 가능성을 높여 상호 연결된 세계에서 프로젝트를 장기적인 성공으로 이끄십시오. 이러한 강력한 도구의 잠재력을 최대한 활용하려면 가독성, 문서화 및 철저한 테스트를 우선시하십시오.