해당 게시글은
강의 사이트, 인프런에서 이정환님이 진행하시는
'한 입 크기로 잘라먹는 타입스크립트' 를
들어보며 블로그를 작성하려고 합니다.
다음 게시글 내용은 해당 강의에 핸드북의 내용으로
출처는 다음과 같습니다.
https://ts.winterlood.com/7250edd7-a3fd-4662-b756-f11f927c73f2
타입스크립트를 소개합니다 - 타입스크립트 개론
한 입 크기로 잘라먹는 타입스크립트
ts.winterlood.com
section9/chapter0.ts 에서 실습을 진행했습니다.
조건부 타입
조건부 타입은 extends 와 삼항 연산자를 이용해
조건에 따라 각각 다른 타입을 정의하도록 돕는 문법입니다.
type A = number extends string ? number : string;
조건부 타입은 위 코드처럼
number extends string ? 과 같은 조건식이 있고,
이 조건이 참이라면 ? 우측의 타입인 Number 타입이 결과가 되고,
아니라면 : 우측의 타입인 String 타입이 결과가 됩니다.
위 조건부 타입의 조건식 number extends string 은
number 타입이 string 타입의 서브타입이 아니기 때문에
거짓이 되고, 그 결과 타입 A는 string 타입이 됩니다.
연습겸 이번엔 조건식에 객체 타입을 사용해 보겠습니다.
type ObjA = {
a: number;
};
type ObjB = {
a: number;
b: number;
};
type B = ObjB extends ObjA ? number : string;
B 는 ObjB 는 ObjA 의 서브타입이므로 조건식이 참이 되어
number 타입이 됩니다.
제네릭 조건부 타입
조건부 타입은 제네릭과 함께 사용할 때 위력이 극대화됩니다.
다음은 타입변수에 Number 타입이 할당되면 String 타입을 반환하고
그렇지 않다면 Number 타입을 반환하는 조건부 타입입니다.
type StringNumberSwitch<T> = T extends number ? string : number;
let varA: StringNumberSwitch<number>; ①
// string
let varB: StringNumberSwitch<string>; ②
// number
① varA 는 T 에 number 타입을 할당합니다. 그 결과 조건식이 참이 되어 string 타입이 됩니다.
② varB 는 T 에 string 타입을 할당합니다. 그 결과 조건식이 거짓이 되어 number 타입이 됩니다.
이번에는 실용적인 예를 살펴보겠습니다.
다음은 매개변수로 String 타입의 값을 제공받아 공백을 제거한 다음
반환하는 함수입니다.
function removeSpaces(text: string) {
return text.replaceAll(" ", "");
}
let result = removeSpaces("hi im winterlood");
이 때 removeSpace 함수의 매개변수에
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
문제가 모두 해결된 것 같습니다.
그런데 한 가지 문제가 있습니다.
변수 result 의 타입이 아까와는 달리 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 로 단언하는 것은 별로 좋지 못하다고 배운 적 있습니다.
예를 들어 다음과 같이 첫 번째 return 문에서 string 이 아닌 타입의 값을 반환해도
오류를 감지하지 못합니다.
function removeSpaces<T>(text: T): T extends string ? string : undefined {
if (typeof text === "string") {
return 0 as any; // 문제 감지 못함
} else {
return undefined as any;
}
}
let result = removeSpaces("hi im winterlood");
// string
let result2 = removeSpaces(undefined);
// undefined
따라서 이럴 때에는 타입 단언보다는 함수 오버로딩을 이용하는 게 더 좋습니다.
오버로드 시그니처의 조건부 타입은 구현 시그니처 내부에서 추론이 가능합니다.
따라서 다음과 같이 오버로드 시그니처를 추가해 함수 오버로딩을 구현합니다.
'TypeScript > 한 입 크기로 잘라먹는 타입스크립트' 카테고리의 다른 글
[조건부 타입] infer (0) | 2023.07.24 |
---|---|
[조건부 타입] 분산적인 조건부 타입 (0) | 2023.07.21 |
[타입 조작하기] 템플릿 리터럴 타입 (0) | 2023.07.20 |
[타입 조작하기] 맵드 타입 (0) | 2023.07.20 |
[타입 조작하기] keyof & typeof 연산자 (0) | 2023.07.20 |