10.1 기본 사용 방법

타입스크립트에서 조건부 타입(Conditional Type)은 조건에 따라 다른 타입을 반환할 수 있는 기능을 제공합니다.

type MyType = T extends U ? X : Y

위의 예제에서 T extends U 이 조건식은 T 타입을 U 타입에 대입할 수 있는지를 검사합니다. 따라서 T 타입이 U 타입의 서브타입이면 이 조건이 참이 되어 X 타입을 반환하고, 그렇지 않으면 Y 타입을 반환합니다. 즉 아래와 같이 NumOrStr 타입을 결정할 수 있습니다.

type NumOrStr = number extends string ? number : string; 
// NumOrStr = string

위의 예제의 조건식은 number 타입이 string 타입의 서브타입인지 확인합니다. 하지만 number 타입은 string 타입의 서브타입이 아니므로 string 타입을 반환합니다.

다음으로 객체 타입을 사용한 조건부 타입 예제를 살펴보겠습니다.

type SuperType = { 
  a: number;
};

type SubType = {
  a: number;
  b: number;
};

type Result = SubType extends SuperType ? number : string;
// Result = number

위의 예제에서 SubType 타입은 SuperType 타입의 서브타입이므로 SubType extends SuperType 조건은 참이 됩니다. 따라서 Result 타입은 number 타입으로 반환됩니다.

10.1.1 제네릭 조건부 타입

제네릭 조건부 타입은 매개변수의 타입이나 특정 조건에 따라 반환 타입을 다르게 설정할 수 있어 코드의 유연성을 높일 수 있습니다. 예를 들어, 아래와 같이 제네릭 조건부 타입을 사용할 수 있습니다.

type ConversionResult<T> = T extends number ? string : number;

let numberToString: ConversionResult<number>; // numberToString: string
let stringToNumber: ConversionResult<string>; // stringToNumber: number

위의 예제에서 ConversionResult<T> 타입은 제네릭 조건부 타입이며, 제네릭 타입 T가 number 타입인지 검사합니다. 만약 이 조건이 만족하면 string 타입을, 그렇지 않다면 number 타입을 반환합니다. 따라서 numberToString는 string 타입이 되고, stringToNumber는 number 타입이 됩니다. 다음으로 더 복잡한 예제를 살펴보겠습니다.

interface Product {
  productName: string;
  hasDiscount: boolean;
}

type ProductWithDiscount = {
  productName: string;
  hasDiscount: true;
  discount?: number;
};

type ProductWithoutDiscount = {
  productName: string;
  hasDiscount: false;
};
// 제품의 할인 여부에 따라 다른 타입을 반환하는 제네릭 조건부 타입
type ProductDiscount<T> = T extends { hasDiscount: true }
  ? ProductWithDiscount
  : ProductWithoutDiscount;

function getProduct<T extends Product>(product: T): ProductDiscount<T> {
	// 할인이 있는 경우
  if (product.hasDiscount) {
    let newProduct = {
      ...product,
      discount: 0.1,
    };

    return newProduct as ProductDiscount<T>;
  }
 // 할인이 없는 경우
  return product as ProductDiscount<T>;
}

// productWithDiscount: ProductWithDiscount
let productWithDiscount = getProduct({
  productName: "컴퓨터",
  hasDiscount: true,
});

//  productWithoutDiscount: ProductWithoutDiscount
let productWithoutDiscount = getProduct({
  productName: "헤드셋",
  hasDiscount: false,
});

위의 예제에서 ProductDiscount<T>getProduct 함수가 반환할 타입을 결정하는 제네릭 조건부 타입이며, 제네릭 타입 T의 객체가 hasDiscount 프로퍼티를 가지고, 그 값이 true 인지 검사합니다. 만약 이 조건을 만족한다면 getProduct 함수는 ProductWithDiscount 타입을 반환하고, 만족하지 않다면 ProductWithoutDiscoun 타입을 반환하게 됩니다. 따라서 getProduct 함수는 입력으로 받은 product 객체의 hasDiscount 값에 따라서, 적절한 타입을 반환할 수 있게 됩니다.

이와 같이 제네릭 조건부 타입을 활용하면 특정 조건에 따라 다른 타입을 반환하는 함수를 유연하게 구현할 수 있습니다. 또한, 여러 유형에 대해 비슷한 작업을 수행해야 할 때 오버로드된 함수를 단일 함수로 대체하는 데에도 유용하게 사용됩니다. 아래의 예시를 살펴보겠습니다.

interface UserIdData {
  id: number;
}
interface UserEmailData {
  email: string;
}

// 함수 오버로딩
function getUserData(inputData: number): UserIdData;
function getUserData(inputData: string): UserEmailData;
function getUserData(inputData: number | string): UserIdData | UserEmailData {
  if (typeof inputData === 'number') {
    return { id: inputData };
  } else {
    return { email: inputData };
  }
}

let userData1 = getUserData(123); // userData1: UserIdData
let userData2 = getUserData("[email protected]"); // userData2: UserEmailData

위의 예제에서 getUserData 함수는 숫자를 입력으로 받으면 UserIdData 타입을 반환하고, 문자열을 입력으로 받으면 UserEmailData 타입을 반환하는 함수 오버로딩이 있습니다. 하지만 같은 로직을 제네릭 조건부 타입을 사용하면 다음과 같이 더 간결하게 표현할 수 있습니다.

type UserData<T extends number | string> = T extends number
  ? UserIdData
  : UserEmailData;

function getUserData<T extends number | string>(inputData: T): UserData<T> {
  if (typeof inputData === "number") {
    return { id: inputData } as UserData<T>;
  } else {
    return { email: inputData } as UserData<T>;
  }
}