Javascript

TypeScript 조건부 타입 완벽 가이드: 유니온 타입과 유틸리티 타입 활용의 모든 것

드리프트2 2025. 3. 15. 19:45

TypeScript 조건부 타입 완벽 가이드: 유니온 타입과 유틸리티 타입 활용의 모든 것

조건부 타입은 TypeScript에서 if-then-else 표현식과 같은 역할을 합니다.

결과는 두 가지 분기 중 하나로 결정되는데요.

이는 특히 제네릭 타입에서 유용하며, 유니온 타입을 다룰 때 "루프"를 돌리는 것처럼 활용할 수 있습니다.

이번 글에서는 조건부 타입의 동작 원리를 자세히 알아보겠습니다.


소스 코드에서 계산되거나 추론된 타입을 보여주기 위해 npm 패키지 asserttt를 사용합니다.

예를 들면 다음과 같습니다:

// 값의 타입 확인  
assertType<string>('abc');  
assertType<number>(123);  

// 타입 동등성 검사  
type Pair<T> = [T, T];  
type _ = Assert<Equal<  
  Pair<string>, [string, string]  
>>;  

문법과 첫 번째 예제

조건부 타입의 기본 문법은 다음과 같습니다:

«Sub» extends «Super» ? «TrueBranch» : «FalseBranch»  

 

조건부 타입은 세 부분으로 구성됩니다:

  1. SubSuper에 할당 가능한지 확인 (조건),
  2. 참일 경우 TrueBranch를 결과로 반환,
  3. 거짓일 경우 FalseBranch를 반환.

긴 조건부 타입은 다음과 같이 가독성 있게 작성하는 것을 추천합니다:

«Sub» extends «Super»  
  ? «TrueBranch»  
  : «FalseBranch»  

 

간단한 예제를 살펴보겠습니다:

type IsNumber<T> = T extends number ? true : false;  
type _ = [  
  Assert<Equal<IsNumber<123>, true>>,  
  Assert<Equal<IsNumber<number>, true>>,  
  Assert<Equal<IsNumber<'abc'>, false>>,  
];  

조건부 타입 체이닝

JavaScript의 삼항 연산자처럼 TypeScript의 조건부 타입도 체이닝이 가능합니다:

type PrimitiveTypeName<T> =  
  T extends undefined ? 'undefined' :  
  T extends null ? 'null' :  
  T extends boolean ? 'boolean' :  
  T extends number ? 'number' :  
  T extends bigint ? 'bigint' :  
  T extends string ? 'string' :  
  never;  

type _ = [  
  Assert<Equal<PrimitiveTypeName<123n>, 'bigint'>>,  
  Assert<Equal<PrimitiveTypeName<bigint>, 'bigint'>>,  
];  

중첩된 조건부 타입

이전 예제에서는 false 분기에 다음 조건부 타입이 중첩되었습니다.

그러나 true 분기에 중첩된 조건부 타입이 있다면 들여쓰기로 가독성을 높일 수 있습니다:

type RemoveEmptyStrings<T extends Array<string>> =  
  T extends [infer First extends string, ...infer Rest extends Array<string>]  
    ? First extends ''  
      ? RemoveEmptyStrings<Rest>  
      : [First, ...RemoveEmptyStrings<Rest>]  
    : [];  

type _ = Assert<Equal<  
  RemoveEmptyStrings<['', 'a', '', 'b', '']>,  
  ["a", "b"]  
>>;  

할당 가능성 검사 예제

조건부 타입을 사용해 할당 가능성을 검사할 수 있습니다:

type IsAssignableFrom<A, B> = B extends A ? true : false;  
type _ = [  
  Assert<Equal<IsAssignableFrom<number, 123>, true>>,  
  Assert<Equal<IsAssignableFrom<number, 'abc'>, false>>,  
];  

.length 프로퍼티를 가진 타입만 래핑하기

다음 예제에서는 .length 프로퍼티가 숫자인 타입만 튜플로 래핑합니다:

type WrapLen<T> = T extends { length: number } ? [T] : T;  
type _ = [  
  Assert<Equal<WrapLen<string>, [string]>>,  
  Assert<Equal<WrapLen<RegExp>, RegExp>>,  
];  

유니온 타입에 대한 조건부 타입 분배성

조건부 타입은 유니온 타입에 대해 분배적으로 동작합니다.

즉, 유니온 타입의 각 요소에 조건부 타입을 적용한 결과의 유니온을 반환합니다:

type WrapLen<T> = T extends { length: number } ? [T] : T;  
type _ = Assert<Equal<  
  WrapLen<boolean | 'hello' | Array<number>>,  
  boolean | ['hello'] | [Array<number>]  
>>;  

 

분배성 제한 방법

분배성을 방지하려면 타입 변수를 배열로 래핑하면 됩니다:

type IsString2<T> = [T] extends [string] ? 'yes' : 'no';  
type _ = [  
  Assert<Equal<IsString2<string>, 'yes'>>,  
  Assert<Equal<IsString2<string | number>, 'no'>>,  
];  

유니온 타입 필터링

never를 활용해 유니온 타입의 특정 요소를 제거할 수 있습니다:

type DropNumber<T> = T extends number ? never : T;  
type _ = Assert<Equal<  
  DropNumber<1 | 'a' | 2 | 'b'>,  
  'a' | 'b'  
>>;  

 

내장 유틸리티 타입 Exclude와 Extract

TypeScript는 ExcludeExtract를 제공합니다:

type Exclude<T, U> = T extends U ? never : T;  
type Extract<T, U> = T extends U ? T : never;  

type Union = 1 | 'a' | 2 | 'b';  
type _ = [  
  Assert<Equal<Exclude<Union, number>, 'a' | 'b'>>,  
  Assert<Equal<Extract<Union, number>, 1 | 2>>,  
];  

조건부 타입과 infer

infer를 사용해 복합 타입의 부분을 추출할 수 있습니다:

type ElemType<Arr> = Arr extends Array<infer Elem> ? Elem : never;  
type _ = Assert<Equal<  
  ElemType<Array<string>>, string  
>>;  

지연된 조건부 타입

아직 값이 결정되지 않은 타입 변수를 포함하는 조건부 타입은 지연된 상태로 남습니다:

type StringOrNumber<Kind extends 'string' | 'number'> =  
  Kind extends 'string' ? string : number;  

function randomValue<K extends 'string' | 'number'>(kind: K) {  
  type Result = StringOrNumber<K>;  
  type _ = Assert<Equal<  
    Result, K extends 'string' ? string : number  
  >>;  
}