DEVELOPMENT/Typescript

[TS] 조건부 타입 (Conditional Types)

Watnu 2025. 3. 5. 17:09



01


조건부 타입 이란?

조건부 타입은 어떤 조건이 참인지 거짓인지에 따라 다른 타입을 반환하는 기능입니다.

type A = number extends string ? number : string;

여기서 number extends string은 거짓이니까, A의 타입은 string이 됩니다

 

💡 예제: 객체 타입 비교

객체 타입에서도 동일한 원리가 적용됩니다.

type ObjA = { a: number };
type ObjB = { a: number; b: number };

type B = ObjB extends ObjA ? number : string;

ObjB는 ObjA를 포함하는 상위 타입이므로, B의 타입은 number가 됩니다.



02


제네릭 조건부 타입

제네릭을 활용하면 타입을 유동적으로 설정할 수 있습니다!

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

let varA: StringNumberSwitch<number>;  // string
let varB: StringNumberSwitch<string>;  // number

여기서 T가 numbe r면 string을 반환하고, 아니면 number를 반환합니다.

제네릭 조건부 타입을 활용하면 다양한 상황에서 유용하게 사용할 수 있습니다.

 

💡 예제: 공백을 제거하는 함수
다음은 문자열의 공백을 제거하는 함수입니다.

function removeSpaces(text: string) {
  return text.replaceAll(" ", "");
}

 

하지만 이 함수는 undefined나 null이 전달될 경우 오류가 발생할 수 있습니다.

이를 해결하기 위해 조건부 타입을 활용할 수 있습니다.

function removeSpaces<T>(text: T): T extends string ? string : undefined {
  if (typeof text === "string") {
    return text.replaceAll(" ", "") as any;  // ❌ 타입 단언 사용
  } else {
    return undefined as any;
  }
}

 

하지만 any를 쓰면 타입 검사가 제대로 안 되니까, 함수 오버로딩을 사용해야 하죠.

function removeSpaces<T>(text: T): T extends string ? string : undefined;
function removeSpaces(text: any) {
  if (typeof text === "string") {
    return text.replaceAll(" ", "");
  } else {
    return undefined;
  }
}

text가 string이면 공백을 제거한 값을 반환하고, 그렇지 않으면 undefined를 반환하도록 설계되었습니다.



03


분산적인 조건부 타입

유니온 타입을 조건부 타입과 함께 사용하면 자동으로 분리됩니다!

type StringNumberSwitch<T> = T extends number ? string : number;
let c: StringNumberSwitch<number | string>;  // string | number

number | string이 각각 따로 평가된 후 합쳐져서 string | number 타입이 됩니다.

이러한 특성을 활용하면 특정 타입을 제거하는 기능도 구현할 수 있습니다.

 

💡 예제:  특정 타입 제거하기

type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<number | string | boolean, string>; // number | boolean

string 타입이 제거되고 number | boolean 타입만 남게 됩니다.



04


infer로 타입 추론하기

infer는 조건부 타입 내부에서 특정 타입을 자동으로 추론할 수 있습니다!

type ReturnType<T> = T extends () => infer R ? R : never;

type FuncA = () => string;
type A = ReturnType<FuncA>;  // string

infer R은 함수의 반환 타입을 자동으로 추론하여 A의 타입을 string으로 변환합니다.

 

💡 예제:  Promise 결괏값 추출하기

다음은 Promise 타입에서 결괏값의 타입을 추출하는 예제입니다.

type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;
type Result = PromiseUnpack<Promise<number>>;  // number

Promise <number> 타입에서 number만 추출하여 Result의 타입이 number가 됩니다.



05


추가 예제

💡 예제:  IsProductKey 타입 구현하기

Product의 key인지 확인하는 타입을 만들기

// Product 인터페이스 정의
interface Product {
  id: number;
  name: string;
  price: number;
  seller: {
    id: number;
    name: string;
    company: string;
  };
}

// Product의 key인지 확인하는 타입
type IsProductKey<T> = T extends keyof Product ? true : false;

// ✅ 테스트
type Test1 = IsProductKey<"id">;  // true
type Test2 = IsProductKey<"name">; // true
type Test3 = IsProductKey<"seller">; // true
type Test4 = IsProductKey<"company">; // false (Product의 직접적인 key가 아님)
type Test5 = IsProductKey<"price">; // true
type Test6 = IsProductKey<"randomKey">; // false

✅ id, name, price, seller는 Product의 키이므로 true
❌ company는 seller의 내부 속성이므로 false


💡 예제:  배열 타입의 요소를 추출하는 InferArrayType <T> 타입 구현하기

배열 T가 Array<infer R> 또는 (infer R)[]의 형태인지 확인하고, 그렇다면 R(요소 타입)을 반환한다.
그렇지 않다면 never을 반환한다.

// 배열의 요소 타입을 추출하는 타입
type InferArrayType<T> = T extends Array<infer R> ? R : never;

/* 또는 같은 의미로 작성 가능 */
type InferArrayType<T> = T extends (infer R)[] ? R : never;

 

✅ 테스트

import { Expect, Equal } from "@type-challenges/utils";

const arr1 = [1, 2, 3]; 
const arr2 = ["hello", "myname", "winterlood"];
const arr3 = [1, true, "hi"];

// 기대 결과와 비교하여 타입 체크
type TestCases = [
  Expect<Equal<InferArrayType<typeof arr1>, number>>, // ✅ number
  Expect<Equal<InferArrayType<typeof arr2>, string>>, // ✅ string
  Expect<Equal<InferArrayType<typeof arr3>, number | string | boolean>> // ✅ 요소 타입이 여러 개면 유니온 타입으로 추출
];

 

💡 예제:  Extract<T, U> 타입 구현하기

T에서 U에 해당하는 타입만 추출하는 타입을 구하기

// Extract<T, U>: T에서 U에 해당하는 타입만 남기는 타입
type Extract<T, U> = T extends U ? T : never;

// ✅ 테스트
type E1 = Extract<"apple" | "banana" | "cherry", "banana" | "orange">;
// 결과: "banana" (교집합)

type E2 = Extract<number | string | boolean, string | boolean>;
// 결과: string | boolean (number는 제거됨)

type E3 = Extract<1 | 2 | 3, 2 | 3 | 4>;
// 결과: 2 | 3 (4는 T에 없어서 제외)

type E4 = Extract<"a" | "b" | "c", "x" | "y">;
// 결과: never (공통되는 타입이 없음)

type E5 = Extract<"frontend" | "backend", "frontend">;
// 결과: "frontend" (일치하는 값만 남음)

✅ Extract<T, U>는 T에서 U에 포함된 타입만 남기는 역할을 하죠!
❌ Exclude<T, U>와 반대로 동작한다는 점을 기억하면 좋습니다!

 

번외) Extract 와 Exclude

타입스크립트에서 Extract<T, U>와 Exclude <T, U>는 유니온 타입에서 특정 타입을 남기거나 제거하는 역할을 합니다.

이 둘은 완전히 반대되는 동작을 합니다.

Extract<T, U> → "U에 포함된 것만 남기기"
Exclude<T, U> → "U에 포함된 것 제거하기"

 

💡 예제: Extract<T, U>

T의 각 요소가 U에 포함되면 유지하고, 아니면 제거하는 것!

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

//동작 과정
type A = Extract<"a" | "b" | "c", "a" | "c">;

// 결과: "a" | "c"

 

💡 예제: Exclude<T, U>

T의 각 요소가 U에 포함되면 제거하고, 아니면 유지하는 것!

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

//동작 과정
type B = Exclude<"a" | "b" | "c", "a" | "c">;

// 결과: "b"


06


마무리

기능 설명
조건부 타입 extends와 ? :을 이용해 조건에 따라 다른 타입을 반환합니다.
제네릭 조건부 타입 입력 타입에 따라 유연하게 동작할 수 있도록 만듭니다.
분산적인 조건부 타입 유니온 타입을 개별적으로 평가한 후 다시 합쳐줍니다.
 infer를 활용한 타입 추론 특정 타입을 자동으로 추출할 수 있습니다.

 

이제 조건부 타입을 이해하고 활용할 수 있을 것입니다! TypeScript 타입 시스템을 적극적으로 활용해 보세요. 😆🔥

 

 

댓글수0