TYPESCRIPT

[TypeScript] TypeScript와 조건부 타입

ch010104 2025. 6. 30. 18:13

1. 기본 문법

// 조건부 타입 -> 3항 연산자를 이용하여 조건에 따라 타입을 변경 
type A = number extends string ? string : number; // number가 string을 확장하는 타입이냐?? 참이면 A는 string 타입, 거짓이면 number 타입
// -> 거짓이기 때문에 A는 number 타입!

2. 객체 타입 비교

type ObjA = { // 슈퍼 타입
  a: number;
};

type ObjB = { // 서브 타입
  a: number;
  b: number;
};

type B = ObjB extends ObjA ? number : string; // 참이기 때문에 B는 number 타입!

3. 제네릭 조건부 타입

// 제네릭과 조건부 타입
type StringNumberSwitch<T> = T extends number ? string : number; // T에 string이 들어오면 조건문이 거짓이 되어 StringNumberSwitch가 number 타입이 됨.
// T에 number이 들어오면 조건문이 거짓이 되어 StringNumberSwitch가 string 타입이 됨.

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

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

function removeSpaces<T>(text: T): T extends string ? string : undefined; // T가 string이면 removeSpaces도 string 타입. 아닐 경우 undefined 타입 -> 오버로드 시그니처

function removeSpaces(text: any) {
  if (typeof text === "string") {
    return text.replaceAll(" ", ""); // " "을 모두 찾아서, ""으로 바꿈
  } else {
    return undefined;
  }
}

let result = removeSpaces("hi im winterlood"); // result: string
result.toUpperCase();

let result2 = removeSpaces(undefined); // result2: undefined
  • 제네릭을 사용하면 입력 타입에 따라 결과 타입이 동적으로 바뀌어 재사용성과 안전성이 높아짐

4. 조건부 타입 적용 (with 오류 발생)

function removeSpaces<T>(text: T): T extends string ? string : undefined {
  if (typeof text === "string") {
    return text.replaceAll(" ", "") as any; // ⛔ 타입 안전성 손상
  } else {
    return undefined as any;
  }
}
 
  • as any를 사용하면 오류는 사라지지만, 타입 안전성이 사라짐

5. 안전한 방법: 함수 오버로딩 사용

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

let result = removeSpaces("hi im winterlood"); // string
let result2 = removeSpaces(undefined);         // undefined
  • 오버로딩을 이용하면 타입 추론도 정확하고 타입 안정성도 유지할 수 있음

6. 분산 조건부 타입 (Distributive Conditional Types)

 
  • 조건부 타입에 유니언 타입을 넣으면 각 타입별로 개별적으로 조건 평가 후 다시 합침
// 분산적인 조건부 타입
type StringNumberSwitch0<T> = T extends number ? string : number;

let a0: StringNumberSwitch0<number>; // a: string
let b0: StringNumberSwitch0<string>; // b: number

let c0: StringNumberSwitch0<number | string>; // c: string | number -> number 만 나와야할 거 같은데?? 왜??
// -> <number | string> 가 동시에 T에 들어가는 것이 아니라, 분산적으로 number랑 string이 따로 들어가서 묶이는 것!
// StringNumberSwitch<number> | StringNumberSwitch<string> -> c0: number 이렇게 작동함!

let d0: StringNumberSwitch0<boolean | number | string>; // d0: string | number
// 1 단계
// StringNumberSwitch<boolean> | StringNumberSwitch<number> | StringNumberSwitch<string>

// 2 단계
// number | string | number

// 결과
// number | string

// 실용적인 예제
type Exclude<T, U> = T extends U ? never : T; // T가 U로 확장된다면 never 아니라면 T 타입

type A = Exclude<number | string | boolean, string>;
// 1 단계
// Exclude<number, string> | Exclude<string, string> | Exclude<boolean, string>

// 2 단계
// number | never | boolean -> never는 공집합과 같은 것이기 때문에 사라짐

// 결과
// number | boolean

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

type B = Exclude<number | string | boolean, string>; // B = number | boolean

// 분산적인 조건부 타입 사용 안하기
type StringNumberSwitch<T> = [T] extends [number] ? string : number; // 이렇게 할 경우 분산하지 않고 넘겨줌 -> 분산적인 조건부 타입을 막음
let c: StringNumberSwitch<number | string>; // <number | string> 자체를 T로 넘김
// c : number

let d: StringNumberSwitch<boolean | number | string>; // d: number

7. infer 키워드 사용법

  • infer는 조건부 타입 안에서 특정 타입을 추론하는 데 사용
// infer
// inference -> 추론하다 -> R을 추론해라
type FuncA = () => string;

type FuncB = () => number;

type ReturnType0<T> = T extends () => string ? string : never; // 함수의 반환값이 string인지만 검사 가능

type A0 = ReturnType0<FuncA>; // A0 = string
// T에 () => string가 들어감 -> 참이므로 string

type B0 = ReturnType0<FuncB>; // B0 = never
// T에 () => number가 들어감 -> 거짓이므로 never

type ReturnType<T> = T extends () => infer R ? R : never; // 함수의 반환값이 string인지 number인지 모두 검사 가능
// T extends () => infer R 가 참이 되는 R 타입을 찾아서 R 타입을 반환

type A = ReturnType<FuncA>; // A = string -> T에 FuncA가 들어감. FuncA 타입이 () => infer R 가 되는 R을 찾아 R 반환

type B = ReturnType<FuncB>; // B = number -> T에 FuncB가 들어감. FuncB 타입이 () => infer R 가 되는 R을 찾아 R 반환

type C = ReturnType<number>; // C = never -> T에 number가 들어감. number 타입이 () => infer R 가 되는 R이 없기 때문에 never 반환

8. Promise 타입에서 결과 추출하기

// 예제
type PromiseUnpack<T> = T extends Promise<infer R> ? R : never; // T 가 Promise<infer R> 인가? 확인
// 1. T는 프로미스 타입이어야 한다.
// 2. 프로미스 타입의 결과값 타입을 반환해야 한다.

type PromiseA = PromiseUnpack<Promise<number>>; // T에 Promise<number>을 전달
// number

type PromiseB = PromiseUnpack<Promise<string>>; // T에 Promise<string>을 전달
// string

9. 깃허브 코드 내용