💡 조건부 타입
• 조건부 타입의 정의
조건부 타입은 extends와 삼항 연산자를 통해 조건에 따라 각각 다른 타입을 정의하도록 돕는 문법입니다.
type A = number extends string ? string : number;
조건부 타입은 number extends string ? 과 같은 조건식이 있고 이 조건이 참이라면 ? 바로 다음에 위치한 타입인 string 타입이 결과가 되고, 거짓이라면 그 다음 타입인 number 타입이 결과가 됩니다. 현재 위 조건부 타입의 조건식은 number 타입이 string 타입의 서브타입이 아니기 때문에 거짓이 되고 그 결과 타입 A는 number 타입이 됩니다.
type ObjA = {
a: number;
};
type ObjB = {
a: number;
b: number;
};
type B = ObjB extends ObjA ? number : string;
ObjB는 ObjA의 서브 타입이므로 조건식은 참이 되므로 타입 B는 number 타입이 됩니다.
• 제네릭 조건부 타입
조건부 타입은 제네릭과 함께 사용하였을 때 더 효과적인 문법입니다.
// 제네릭 + 조건부 타입
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 타입이 됩니다.
function removeSpaces(text: string) {
return text.replaceAll(" ", "");
}
let result = removeSpaces("hi im shyunu");
다음은 매개변수로 string 타입의 값 text를 제공받아 공백을 제거하여 반환하는 removeSpaces 함수입니다.
function removeSpaces(text: string | undefined | null) {
return text.replaceAll(" ", ""); // ❌ text가 string이 아닐 수도 있습니다.
}
이때 이 removeSpaces 함수의 매개변수에 undefined 이나 null 타입의 값이 될 수 있다고 가정하고 매개변수의 타입을 수정해주었습니다. 이렇게 수정하면 함수 내부에서 text의 타입은 문자열이 아닐 수도 있기 때문에 오류가 발생합니다.
function removeSpaces(text: string | undefined | null) {
if (typeof text === "string") {
return text.replaceAll(" ", "");
} else {
return undefined;
}
}
let result = removeSpaces("hi im shyunu"); // string | undefined
이렇게 typeof 연산자를 사용하여 타입을 좁혀서 사용하면 오류를 해결할 수 있습니다. 하지만 변수 result의 타입이 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 shyunu"); // string
let result2 = removeSpaces(undefined); // undefined
이러한 경우에는 조건부 타입을 이용하여 인수로 전달된 값의 타입이 string 이면 반환값 타입도 string 이고 아니라면 반환값 타입을 undefined 으로 만들면 됩니다. 다음과 같이 타입변수 T를 추가하고 매개변수의 타입을 T로 정의한 다음, 반환값의 타입을 T extends string ? string : undefined 으로 수정하였습니다.
이제부터 변수 result 처럼 인수로 string 타입의 값을 전달하면 조건부 타입에 따라 반환값의 타입이 string이 되고, 변수 result2 처럼 인수로 undefined를 전달하면 반환값의 타입도 undefined가 될 것입니다.
하지만, 2개의 return문 모두 에러가 발생하였습니다. 왜냐하면 조건부 타입의 결과를 함수 내부에서는 알 수 없기 때문입니다.
function removeSpaces<T>(text: T): T extends string ? string : undefined {
if (typeof text === "string") {
return text.replaceAll(" ", "") as any;
} else {
return undefined as any;
}
}
따라서 다음과 같이 타입 단언을 이용하여 반환값의 타입을 any 타입으로 단언하였습니다. 그러면 에러가 더이상 발생하지 않습니다. 하지만 any로 타입을 단언하는 것은 좋은 코드가 아닙니다. 이러한 상황에서는 타입 단언보다는 함수 오버로딩을 사용하는 것을 추천합니다. 오버로드 시그니쳐의 조건부 타입은 구현 시그니쳐 내부에서 추론이 가능합니다.
function removeSpaces<T>(text: T): T extends string ? string : undefined;
function removeSpaces<T>(text: any) {
if (typeof text === "string") {
return text.replaceAll(" ", "");
} else {
return undefined;
}
}
오버로드 시그니쳐를 추가하여 함수 오버로딩을 구현하였습니다.
💡 분산적인 조건부 타입
• 분산적인 조건부 타입의 정의
type StringNumberSwitch<T> = T extends number ? string : number;
let c: StringNumberSwitch<number | string>;
타입 변수에 유니온 타입을 할당하였습니다. 지금까지 배운 조건부 타입 문법에 따르면 number | string은 number의 서브타입이 아니므로 조건식이 거짓이 되어 변수 c의 타입은 number가 될 것이라고 예상됩니다. 그러나 변수 c는 string | number 타입으로 정의됩니다. 왜 이렇게 될까요?
그 이유는 조건부 타입의 타입 변수에 Union 타입을 할당하면 분산적인 조건부 타입으로 조건부 타입이 업그레이드 되기 때문입니다.
StringNumberSwitch<number | string>
→ StringNumberSwitch<number> | StringNumberSwitch<string>
→ string | number
다음은 분산적인 조건부 타입의 과정입니다.
let d: StringNumberSwitch<boolean | number | string>;
// 분산 과정
StringNumberSwitch<boolean> | StringNumberSwitch<number> | StringNumberSwitch<string>
→ number | string | number
→ number | string (중복값은 제거)
타입이 3가지가 포함된 유니온 타입의 변수 d의 경우도 다음과 같은 과정으로 분산이 이루어집니다.
• Exclude 조건부 타입 구현하기
특정 유니온 타입으로부터 특정 타입만 제거한 유니온 타입을 추출할 수 있습니다.
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<number | string | boolean, string>;
다음과 같은 코드는 다음의 흐름으로 동작합니다.
⓵ Union 타입의 분리
Exclude<number, string> | Exclude<string, string> | Exclude<boolean, string>
⓶ 각 분리된 타입의 계산
• T = number, U = string → number extends string (거짓) → number
• T = string, U = string → string extends string (참) → never
• T = boolean, U = string → boolean extends string (거짓) → boolean
⓷ 2번 과정의 타입들을 Union으로 묶기
• 결과 : number | never | boolean
이와 같은 계산 과정으로 타입 A는 number | never | boolean 타입으로 정의됩니다. 하지만 never 타입은 공집합을 의미하기 때문에 Union으로 묶일 경우 사라집니다. 왜냐하면 공집합과 어떤 집합의 합집합은 그냥 원본 집합이 되기 때문입니다.
따라서, 최종적으로 타입 A는 number | boolean 타입이 됩니다.
• Extract 조건부 타입 구현하기
특정 유니온 타입으로부터 특정 타입만을 추출할 수도 있습니다.
type Extract<T, U> = T extends U ? T : never;
type B = Extract<number | string | boolean, string>;
다음과 같은 코드는 다음의 흐름으로 동작합니다.
⓵ Union 타입의 분리
Extract<number, string> | Extract<string, string> | Extract<boolean, string>
⓶ 각 분리된 타입의 계산
• T = number, U = string → number extends string (거짓) → never
• T = string, U = string → string extends string (참) → string
• T = boolean, U = string → boolean extends string (거짓) → never
⓷ 2번 과정의 타입들을 Union으로 묶기
• 결과 : never | string | never
이와 같은 계산 과정으로 타입 B는 never | string | never 타입으로 정의됩니다. 하지만 never 타입은 위에서 설명한대로 공집합을 의미하기 때문에 Union으로 묶일 경우 사라집니다.
따라서, 최종적으로 타입 B는 string 타입이 됩니다.
💡 infer
infer는 조건부 타입 내에서 특정 타입만 추론할 수 있는 기능입니다. 특정 함수 타입에서 반환값의 타입만을 추출하는 특수한 조건부 타입인 ReturnType을 만들 때 사용할 수 있습니다.
type FuncA = () => string;
type FuncB = () => number;
type ReturnType<T> = T extends () => infer R ? R : never;
type A = ReturnType<FuncA>; // string
type B = ReturnType<FuncB>; // number
조건식 T extends ( ) => infer R에서 infer R은 이 조건식을 참이 되도록 만들 수 있는 최적의 R 타입을 추론하라는 의미입니다. 다음은 A 타입을 계산할 때의 코드의 흐름입니다.
⓵ 타입 변수 T에 함수 타입 FuncA가 할당됩니다.
⓶ T는 ( ) => string 이 됩니다.
⓷ 조건부 타입의 조건식은 다음과 같습니다. ( ) => string extends ( ) => infer R ? R : never
⓸ 조건식을 참으로 만드는 R 타입을 추론한 결과 R은 String입니다.
⓹ 추론이 가능하면 이 조건식을 참으로 판단하며 결과는 String이 됩니다.
type C = ReturnType<number>; // 추론 불가능 → never
하지만, 다음과 같은 상황에서는 추론이 불가능하기 때문에 조건식을 거짓으로 판단합니다.
✱ 예제) Promise의 resolve 타입을 infer를 이용하여 추출하기
// 예제
// 1. T는 프로미스 타입이어야한다.
// 2. 프로미스 타입의 결과값을 반환해야한다.
type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;
type PromiseA = PromiseUnpack<Promise<number>>; // number
type PRomiseB = PromiseUnpack<Promise<string>>; // string
출처) 한 입 크기로 잘라먹는 타입스크립트(TypeScript)_이정환
'📍 프로그래밍 언어 > TypeScript' 카테고리의 다른 글
[ TypeScript ] 유틸리티 타입 - 맵드 타입 & 조건부 타입 (1) | 2024.12.24 |
---|---|
[ TypeScript ] 타입 조작하기 2 - 맵드 타입, 템플릿 리터럴 타입 (0) | 2024.12.23 |
[ TypeScript ] 타입 조작하기 1 - 인덱스드 액세스 타입, keyof & typeof 연산자 (0) | 2024.12.23 |
[ TypeScript ] 제네릭으로 확장하기: 인터페이스, 클래스, 프로미스 (0) | 2024.12.22 |
[ TypeScript ] 제네릭 이해하기: 타입 변수와 메서드 타입 정의(map, forEach) (1) | 2024.12.22 |