Typescript

조건문과 비슷한 컨디셔널 타입이 있다

비숑주인 2025. 3. 29. 20:11

TypeScript에는 타입을 조건에 따라 유연하게 지정할 수 있는 컨디셔널 타입(Conditional Type) 문법이 존재한다. 마치 자바스크립트의 삼항 연산자처럼 동작하지만, 타입 레벨에서 조건 분기를 처리할 수 있어 타입 설계에 유용하다.

 

기본 문법

type A1 = string;
type B1 = A1 extends string ? number : boolean; // number

type A2 = number;
type B2 = A2 extends string ? number : boolean; // boolean

 

여기서 extends는 A2가 string 타입에 할당될 수 있는가? 를 검사한다. 할당 가능하다면 참(true), 아니면 거짓(false)이 되어 삼항 연산자에 따라 타입이 결정 된다. 

 

명시적인 extends가 없어도 OK

interface X { x: number }
interface XY { x: number; y: number }

type A = XY extends X ? string : number; // string

 

구조적 타이핑 덕분에 XY는 X를 명시적으로 extends하지 않았지만, 구조적으로 X에 할당 가능하므로 조건이 true가 되어 A는 string 타입이 된다

 

타입 검사로 활용

type Result1 = 'hi' extends string ? true : false; // true
type Result2 = [1] extends [string] ? true : false; // false

 

리터럴 값이 특정 타입에 해당하는지를 검사하는 데도 쓸 수 있다. 

 

never와 함께 쓰기

type Start = string | number;
type New = Start extends string | number ? Start[] : never; // string[] | number[]

 

이건 그냥 Start[]로 해도 되지만, 실제로는 제네릭 타입과 함께 쓸 때 never가 의미를 가진. 단순한 extends 조건문에서 never을 쓰는 것보다, 제네릭과 결합했을 때 never가 의도한 동작을 정확히 만들어낼 수 있는 도구가 된다는 뜻이다. 

 

type ChooseArray<A> = A extends string ? string[] : never;

type StringArray = ChooseArray<string>; // string[]
type Never = ChooseArray<number>; // never

 

매핑된 타입과 함께 쓰기

type OmitByType<O, T> = {
  [K in keyof O as O[K] extends T ? never : K]: O[K];
};

type Result = OmitByType<{
  name: string;
  age: number;
  married: boolean;
  rich: boolean;
}, boolean>;
// { name: string; age: number }

 

OmitByType은 객체에서 특정 타입의 속성을 제거해 준다. 조건이 true일 때 키를 never로 만들어 제거하는 원리이다. 

 

중첩된 컨디셔널 타입

type ChooseArray<A> = 
  A extends string ? string[] :
  A extends boolean ? boolean[] :
  never;

type Result1 = ChooseArray<string>; // string[]
type Result2 = ChooseArray<boolean>; // boolean[]
type Result3 = ChooseArray<number>; // never

 

삼항 연산자처럼 중첩해서 사용도 가능하다. 

 

인덱스 타입 접근 방식으로도 가능

type A1 = string;
type B2 = {
  t: number;
  f: boolean;
}[A1 extends string ? 't' : 'f']; // number

 

조건 분기의 결과가 문자열일 경우, 이를 객체의 키로 활용하여 타입을 지정할 수도 있다. 

 

분배 법칙(Distributive Conditional Types)

type Result<T> = T extends string ? T[] : never;

type R = Result<string | number>; // string[]

 

유니언 타입과 컨디셔널 타입이 만나면 분배법칙이 적용되어 Result<string> | Result<number>로 평가 된다. 즉, 각각 분기한 결과를 다시 합치는 것이다. 

 

type R<T> = T extends string ? true : false;
type RR = R<'hi' | 3>; // true | false → boolean

 

예상과 달리 boolean이 된 이유는 분배법칙 때문이다. 이를 방지하려면 배열로 감싸 분배법칙을 차단할 수 있다. 

 

type IsString<T> = [T] extends [string] ? true : false;
type R = IsString<'hi' | 3>; // false

 

never는 특이 케이스

type R<T> = T extends string ? true : false;
type RR = R<never>; // never

 

never는 분배법칙의 결과가 아무 것도 없기 때문에 결과도 never가 된다. 

 

type IsNever<T> = [T] extends [never] ? true : false;

type T1 = IsNever<never>;     // true
type T2 = IsNever<'never'>;  // false

 

실전 주의점

function test<T>(a: T) {
  type R<T> = T extends string ? T : T;
  const b: R<T> = a; // ❌ Type 'T' is not assignable to type 'R<T>'
}

 

TypeScript는 제네릭이 들어간 컨디셔널 타입을 미루어 판단한다. 이로 인해 a의 타입이 R<T>에 맞지 않는다는 오류가 생길 수 있다.  이럴 때도 배열로 감싸면 해결할 수 있다. 

 

function test<T extends ([T] extends [string] ? string : never)>(a: T) {
  type R<T> = [T] extends [string] ? T : T;
  const b: R<T> = a; // ✅ OK
}

 

TypeScript의 컨디셔널 타입은 복잡한 타입 로직을 간결하고 안전하게 표현할 수 있다.특히 extends, never, 유니언 타입, 제네릭과 조합했을 때 다양한 활용이 가능하기 때문에 타입 설계에서 고려하자.