[TS] 조건부 타입

2024. 2. 22. 16:36· Front-end/TypeScript
목차
  1. 1. 조건부 타입
  2. 1.1 제네릭 조건부 타입
  3. 2. 분산적인 조건부 타입
  4. 2.1 Exclude 조건부 타입 구현하기
  5. 3. infer 

 

1. 조건부 타입

type A = number extends string ? number : string;
  • 조건부 타입은 extends와 삼항 연산자를 이용해 조건에 따라 각각 다른 타입을 정의하도록 돕는 문법이다. 
  • number extends string 조건식이 참이라면 number 타입, 거짓이면 string 타입이된다. 
  • number extends string은 number 타입이 string 타입의 서브 타입이 아니기 때문에 거짓이 되고 type A는 string 타입이된다. 

 

type ObjA = {
  a: number;
};

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

type B = ObjB extends ObjA ? number : string;
  • 다른 예제로 조건식에 객체 타입을 사용해보자.
  • objB는 objA의 서브 타입이므로 조건식이 참이되어 typeB는 number 타입이된다. 

 

1.1 제네릭 조건부 타입

예제 1.

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

let varA: StringNumberSwitch<number>;
// string

let varB: StringNumberSwitch<string>;
// number
  • 조건부 타입은 제네릭과 함께 사용할 때 활용성이 더 높아진다. 
  • 위의 예제는 타입 변수에 number 타입이 할당되면 string 타입을 반환하고 그렇지 않다면 number 타입을 반환하는 조건부 타입이다.
  • varA는 T에 number 타입을 할당한다. 그 결과 조건식이 참이 되어 string 타입이 된다. 
  • varB는 T에 string 타입을 할당한다. 그 결과 조건식이 거짓이 되어 number 타입이 된다. 

 

예제 2.

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

let result = removeSpaces("hi im winterlood");
  • 매개변수로 string 타입의 값을 제공받아 공백을 제공한 다음 변환하는 함수가 있다. 

 

이때 removeSpaces 함수의 매개변수에 undefined 나 null 타입의 값들도 제공될 수 있다고 가정한다면?

function removeSpaces(text: string | undefined | null) {
  return text.replaceAll(" ", ""); // ❌ text가 string이 아닐 수 있음
}

let result = removeSpaces("hi im winterlood");
  • 수정해주어도 함수 내부 text의 타입이 string이 아닐 수 있기 때문에 오류가 발생한다. 
  • 따라서 타입을 좁혀 사용해 줘야한다. 

 

function removeSpaces(text: string | undefined | null) {
  if (typeof text === "string") {
    return text.replaceAll(" ", "");
  } else {
    return undefined;
  }
} 

let result = removeSpaces("hi im winterlood");
// string | undefined
  • 발생하던 오류는 해결되었지만 변수 rerult 타입이 아까와 달리 string | undefined 타입으로 추론된다. 

 

이때 조건부 타입을 이용해 인수로 전달된 값의 타입이 string 이면 반환값 타입도 string이고 아니라면 undefined로 만들어준다. 

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

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

let result2 = removeSpaces(undefined);
// undefined
  • 타입변수 T를 추가하고 매개변수의 타입을 T로 정의한 다음 반환값의 타입을 T extends string ? string : undefined로 해준다. 
  • 변수 result 처럼 인수로 string 타입의 값을 전달하면 조건부 타입에 따라 반환값의 타입이 string이 된다. 
  • 변수 result2 처럼 인수로 undefined 타입의 값을 전달하면 반환값이 undefined가 된다. 
  • 하지만 이때 2개의 return문 모두 오류가 발생한다. 이는 조건부 타입의 결과를 함수 내부에서 알 수 없기 때문이다. 

 

따라서 타입 단언을 이용해 반환값의 타입을 any 타입으로 단언한다. 

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

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

let result2 = removeSpaces(undefined);
// undefined
  • 일단 오류는 해결된듯하지만 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;
  }
}

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

let result2 = removeSpaces(undefined);
// undefined
  • 오버로드 시그니쳐의 조건부 타입은 구현 시그니쳐 내부에서 추론 가능하다. 
  • 오버로드 시그니쳐를 추가해 함수 오버로딩을 구현하여 해결하는것이 좋다. 

 

2. 분산적인 조건부 타입

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

let a: StringNumberSwitch<number>;
let b: StringNumberSwitch<string>;
let c: StringNumberSwitch<number | string>;
// string | number
  • 조건부 타입에서 변수 a의 타입은 조건식이 참이되어 string으로 정의되고 변수 b는 거짓이 되어 number 타입으로 정의되었다. 
  • 타입 변수에 union 타입을 할당하면 변수 c의 타입 number | string은 number의 서브 타입이 아니므로 조건식이 거짓이 되어 number가될거라고 예상할수있다. 하지만 변수 c의 타입은 number | string 값으로 정의된다. 

 

❓ 왜일까?

  • 조건부 타입의 타입 변수에 union 타입을 할당하면 분산적인 조건부 타입으로 조건부 타입이 업그레이드되기 때문이다. 
  • 분산적인 조건부 타입은 타입변수에 할당한 union 타입 내부의 모든 타입이 분리된다.
  • 따라서 StringNumberSwitch<number | string> 타입은 다음과 같이 분산된다. 
    • StringNumberSwitch<number>
    • StringNumberSwitch<string>
  • 그리고 난뒤 분산된 각 타입의 결과를 모아 다시 union 타입으로 묶는다. → number | string

 

2.1 Exclude 조건부 타입 구현하기

  • 분산적인 조건부 타입의 특징을 이용하면 다양한 타입을 정의할 수 있다. 
  • 예제와 같이 union 타입으로부터 특정 타입만 제거하는 Exclude 타입을 정의할 수 있다. 

 

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

type A = Exclude<number | string | boolean, string>;
  1. union 타입이 분리된다.
    • Exclude<number, string>
    • Exclude< string , string>
    • Exclude<boolean, string>
  2. 각 분리된 타입을 모두 계산한다. 
    • T = number, U = string 일 때 number extends string은 거짓이므로 결과는 number
    • T = string, U = string 일 때 string extends string은 참이므로 결과는 never
    • T = boolean, U = string 일 때 boolean extends string은 거짓이므로 결과는 boolean
  3. 계산된 타입들을 모두 union으로 묶는다.
    • 결과 : number | never | boolean

 

계산 결과 타입A는 number | never | boolean 타입으로 정의된다. 이때 never 타입은 union으로 묶일 경우 사라진다. 공집합과 어떤 집합의 합집합은 그냥 원본집합이 되기 때문이다. 따라서 최종적으로 타입 A는 number | boolean 타입이된다. 

 

3. infer 

  • infer는 조건부 타입 내에서 특정 타입을 추론하는 문법이다. 
  • 특정 함수 타입에서 반환값의 타입만 추출하는 특수한 조건부 타입인 ReturnType을 만들 때 이용할 수 있다. 

 

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

type FuncA = () => string;
type FuncB = () => number;

type A = ReturnType<FuncA>;
// string
type B = ReturnType<FuncB>;
// number
type C = ReturnType<number>;
// 조건식을 만족하는 R추론 불가능
// never
  • 조건식 T extends () => infer R 에서 infer R은 이 조건식이 참이 되도록 만들 수 있는 최적의 R 타입을 추론하라는 의미이다.
  • 만약 추론이 불가능하다면 조건식을 거짓으로 판단한다. 

 

💡 A 타입을 추론할 때 이런 흐름을 갖는다.

  1. 타입 변수 T에 함수 타입 FuncA가 할당된다. 
  2. T는 () => string 이 된다.
  3. 조건부 타입의 조건식은 () => string extends () => infer R ? R : never 가 된다.
  4. 조건식을 참으로 만드는 R 타입을 추론한다. 그 결과 R은 string이 된다. 
  5. 추론이 가능하면 이 조건식을 참으로 판단한다. 따라서 결과는 string이 된다. 

 

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

type PromiseA = PromiseUnpack<Promise<number>>;
// number
type PromiseB = PromiseUnpack<Promise<string>>;
// string
  • Promise의 resolve 타입을 infer를 이용해 추출할 수도 있다. 

 

📌 

한 입 크기로 잘라먹는 타입스크립트(TypeScript)

 

저작자표시 (새창열림)

'Front-end > TypeScript' 카테고리의 다른 글

[TS] 유틸리티 타입  (1) 2024.02.27
[TS] 타입 조작하기  (0) 2024.02.22
[TS] 제네릭에 대하여  (0) 2024.02.21
[TS] 함수와 타입  (0) 2024.02.20
[TS] 타입을 추론하고... 단언하고... 좁히고...  (0) 2024.02.19
  1. 1. 조건부 타입
  2. 1.1 제네릭 조건부 타입
  3. 2. 분산적인 조건부 타입
  4. 2.1 Exclude 조건부 타입 구현하기
  5. 3. infer 
'Front-end/TypeScript' 카테고리의 다른 글
  • [TS] 유틸리티 타입
  • [TS] 타입 조작하기
  • [TS] 제네릭에 대하여
  • [TS] 함수와 타입
개발중인제이
개발중인제이
개발중인제이
제이로그
개발중인제이
전체
오늘
어제
  • 분류 전체보기 (100)
    • 기록 (6)
      • 회고 (1)
      • TIL (4)
      • 일상 (0)
      • 리뷰 (1)
    • Web (4)
    • Front-end (81)
      • HTML & CSS (4)
      • JavaScript (9)
      • 모던자바스크립트 (14)
      • React (21)
      • Next (6)
      • TypeScript (11)
      • git & github (2)
      • etc (14)
      • 프로젝트 (0)
    • Back-end (0)
    • 알고리즘 & 자료구조 (0)
    • 패스트캠퍼스 (8)
    • 기타 (1)

블로그 메뉴

  • 홈
  • 글쓰기
  • 관리

공지사항

인기 글

최근 글

hELLO · Designed By 정상우.v4.2.2
개발중인제이
[TS] 조건부 타입
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.