본 포스팅은 Inflearn(인프런) 이정환님의 한 입 크기로 잘라먹는 Typescript 강의를 참고하여 작성되었습니다.
[조건부 타입]
조건부 타입이란 JS의 삼항 연산자를 사용해서 타입을 정의하는 방법을 말한다. Extends 키워드와 삼항 연산자를 함께 사용하여 해당 타입이 특정 타입의 확장을 받아 참, 거짓을 판별한 후 타입을 지정해주는 방식이다. 원시 타입으로 예시를 들면 다음과 같다.
type A = number extends string ? string : number; //typeof A === number
type ObjA = {
a:number
}
type ObjB = {
a:number,
b:number
}
type B = ObjB extends ObjA ? number : string //typeof B === number
A의 경우, number 타입은 string 타입의 서브 타입이 아니기 때문에 삼항 연산자를 통해서 나온 결과 false에 의해서 number가 된다.
B의 경우 ObjB 타입은 ObjA 타입의 서브 타입이므로 삼항 연산자를 통해 나온 결과는 true로 B는 number 타입이 된다.
하지만 조건부 타입은 이런 원시 타입 같은 기본적인 경우에서는 사용하지 않는다.
조건부 타입은 일반적으로 제네릭과 함께 사용하여 가변적이고 논리적인 흐름에 따라서 사용하기에 유용하다.
제네릭과 함께 사용한 경우는 아래와 같은 형태이다.
type Switch<T> = T extends number ? number : string;
let a: Switch<number>;
let b: Switch<string>;
제네릭에 의해 정의된 타입에 따라서 변수의 타입을 결정 짓게 된다.
또한 함수에서도 함수 오버로딩을 이용해서 오버로딩 시그니쳐와 구현 시그니쳐를 함께 사용하여 사용이 가능하다.
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("Hello World");
result.toUpperCase();
let result1 = removeSpaces(undefined);
가장 먼저 함수 오버로딩을 통해서 오버로딩 시그니쳐를 구현하고 그에 맞게 구현 시그니쳐를 작성하였다. any로 정의되어 있는 매개변수인 text의 타입이 무엇인가에 따라 조건부로 타입이 결정되어 로직에 맞게 반환값을 사용할 수 있게 된다.
text의 타입이 string이라면 조건부 타입과 제네릭을 통해 text는 string 타입으로 정의되어 String Type Method를 사용가능하게 되고, undefined인 경우는 undefined 타입으로 정의되어 undefined를 반환하게 된다.
[분산적 조건부 타입]
분산적 조건부 타입이란 앞서 살펴본 조건부 타입과 제네릭을 사용하는 경우에서 특정 변수를 선언할 때 변수의 타입이 Union Type으로 정의된 경우 타입이 정의되는 방식을 의미한다. 아래의 간단한 코드를 보자.
type A<T>: T extends number ? number : string;
let a:A<number>; //typeof a === number
let b:A<string>; //typeof b === string
let c:A<number | string>; //?
위의 코드에서 변수 a,b는 앞서 살펴본 제네릭과 조건부 타입의 혼용 방식으로 타입이 정의되었다. 하지만 변수 c의 경우에는 제네릭 타입이 Union Type으로 정의가 되어있다. 이러한 경우는 어떻게 적용될까.
이런 경우 적용되는 방식이 바로 분산적 조건부 타입이다. 제네릭 문법에 의해서 Union Type으로 정의된 타입이 제네릭에 적용되어 타입을 판단하는 것이 아니라 Union Type을 분산적으로 판단한다. 위의 코드를 예시로 보자면 number 타입이 먼저 제네릭을 거쳐서 조건부 타입이 적용되고 그 이후 순차적으로 string 타입이 제네릭을 거쳐서 조건부 타입을 거친 후 두 가지 결과값이 만나 다시 Union Type으로 합쳐지는 것이다. 분산적 조건부 타입의 생성 과정을 코드로 살펴 보자.
type Switch<T> = T extends number ? string : number;
let c:Switch<number | string>;
/*
* 1.Switch<number> => string
* 2.Switch<string> => number
* typeof c === <string | number>
*/
let d:Switch<boolean | number | string>;
/*
* 1.Switch<boolean> => number
* 2.Switch<number> => string
* 3.Switch<string> => number
* typeof d === <string | number>
*/
위와 같이 Union Type으로 정의된 타입을 하나씩 제네릭에 적용되어 조건부 타입을 거친 후 반환된 타입들이 다시 Union Type으로 생성되어 변수의 타입으로 정의가 되는 것이다.
실용적으로 사용되는 예제를 다음 코드로 한 번 이해해보자.
type Exclude<T,U> = T extends U ? never : T;
type A = Exclude<number | string | boolean , string>
/**
* 1단계
* Exclude<number, string>
* Exclude<string, string>
* Exclude<boolean, string>
* 2단계
* number
* never
* boolean
* 3단계
* <number | boolean>
*/
type Extract<T,U> = T extends U ? T : never;
type B = Extract<number | string | boolean, string>;
/**
* 1단계
* Extract<number, string>
* Extract<string, string>
* Extract<boolean, string>
* 2단계
* never
* string
* never
* 3단계
* string
*/
Union Type으로 정의된 타입과 제네릭으로 두가지 타입을 받았을 경우 분산적으로 타입 정의가 어떻게 진행되는지 과정을 함께 작성하였다. 위의 예시에서 never 타입은 모든 타입의 서브 타입으로 별도로 적용이 되지 않기 때문에 자동적으로 사라지는 것을 볼 수 있다.
분산 방지
분산적 조건부 타입에서 타입의 분산을 방지하는 방법도 존재한다. 간단하게 제네릭 타입에 대괄호를 씌워주면 된다.
type Exclude<T,U> = [T] extends [U] ? never : T;
type A = Exclude<number | string | boolean , string> //typeof A === string | number | boolean
위와 같이 제네릭에 해당하는 T와 U에 대괄호를 씌워주면 분산적으로 타입 정의가 되지 않아 Union Type으로 정의된 타입이 하나로 묶여 조건부 타입에 적용이 되는 것을 볼 수 있다.
[Infer]
Infer란 Inference라는 단어의 약자로 추론을 의미한다. 이 말은 Infer라는 키워드를 통해 타입을 추론하여 가져올 수 있다는 것을 의미한다. 우리가 함수를 사용할 때 만약 함수의 반환값의 타입만 가져와서 다른 타입으로 적용을 하고 싶은 경우 어떻게 처리를 해 줄 수 있을까. 아래 코드와 같이 사용할 수 있을 것 같다.
type Func = () => string;
type Return<T> = T extends () => string ? string : never;
type A = Return<Func>; // typeof A === string
하지만 다양한 함수의 타입이 정의되어 있고 함수의 반환값의 타입을 반환해주는 타입이 위와 같이 Return처럼 하나만 존재하는 경우는 어떻게 할까. 이런 경우에 Infer을 사용한다.
type Func = () => string;
type FuncA = () => number;
type Return<T> = T extends () => infer R ? R : never;
type A = Return<Func>; // typeof A === string
type B = Return<FuncA>; //typeof B === number
위와 같이 우리가 이전에 제네릭에서 T,U 같은 문자를 사용하였듯이 infer과 R이라는 문자를 사용하여 조건부 타입으로 정의하면 제네릭으로 함수 타입을 받게 되고 infer R은 반환값의 타입을 올바르게 반환받을 수 있도록 알맞게 추론하여 반환을 해준다.
정리하면 Type A 같은 경우는 () => string에 맞게 Infer R이 추론을 하여 R에 string이 정의가 되어 반환되는 것이고, Type B 같은 경우는 () => number에 맞게 Infer R이 number로 추론을 하여 R에 number가 정의되어 반환되는 것이다.
하지만 새로운 타입을 정의하면서 아래 예시처럼 제네릭에 특정 타입을 넣어주게 되면 Infer은 타입을 추론하지 못하고 삼항 연산자는 false를 반환하게 되어 조건부 타입에서 Type C에게 never을 정의하게 된다.
type Return<T> : T extends () => infer R ? R : never;
type C = Return<number>; //typeof C === never
간단한 다른 예시도 하나 보면서 마무리 해보자.
type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;
/**
* 1.T는 Promise 타입이어야한다.
* 2. Promise 타입의 결과값 타입을 반환해야한다.
*/
type PromiseA = PromiseUnpack<Promise<number>>; //typeof PromiseA === number
type PromiseB = PromiseUnpack<Promise<string>>; ////typeof PromiseB === string
'Typescript' 카테고리의 다른 글
[TypeScript] - (10) Utility Type (1) | 2024.09.04 |
---|---|
[TypeScript] - (8) Type 조작 (0) | 2024.08.14 |
[TypeScript] - (7) Generic(제네릭) (2) | 2024.08.07 |
[TypeScript] - (6) Class (0) | 2024.08.05 |
[Typescript] - (5) Interface (0) | 2024.08.01 |