위 글
은 타입스크립트의 조건부 타입 을 사용해보고 정리하는 글입니다.
조건부 타입(conditional type)
은 입력된 제네릭 타입(T)에 따라 타입을 결정하는 것을 뜻한다.T extends U ? X : Y
위의 예시에서 보는 것처럼 타입스크립트는 삼항연산자
를 사용하여 값 대신 타입을 조건에 따라 결정한다.
타입스크립트에서 사용하는 extends
에 대해 알아보면서 조건부 타입의 extends는 어떻게 사용되는지 알아보자.
1. interface(인터페이스 확장)
interface Person {
name: string;
}
interface Developer extends Person {
langauge: string;
}
let fe: Developer = {
name: 'poo',
language: 'Typescript'
};
2. Generic(타입의 제한)
T extends K
의 형태의 제너릭이 있다면 T가 K에 할당 가능해야한다고 정의해야한다.
type numOrStr = number | string;
function testDataType<T extends numbOrStr>(data: T) {
console.log(data);
};
testDataType(1);
testDataType('a');
testDataType(true); // ERROR
testDataType([]); // ERROR
3. 조건부 타입(타입의 제한)
제너릭의 extends와 조건부 타입의 extends는 타입을 제한한다는 의미는 동일하다. (새로운 타입을 정의할 때만 사용 가능)
하지만 조건부 타입은 제너릭의 <>
안에 선언할 수 없다.
// X - 선언 시 에러가 발생한다.
interface isDataString2<T extends true ? string : number> {
data: T
}
// O
interface isDataString<T extends boolean> {
data: T extends true ? string : number;
isString: T;
}
const str: isDataString<true> = {
data: '홍길동', // String
isString: true,
};
const num: isDataString<false> = {
data: 9999, // Number
isString: false,
};
앞서 조건부 타입에서 extends에는 타입에 제한
한다는 의미로 사용되었다.
아래의 코드는 어떻게 동작하는지 한번 보도록 하자.
type Diff<T, U> = T extends U ? never : T;
type Filter<T, U> = T extends U ? T : never;
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // ?
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // ?
앞선 조건부 타입의 설명에 따르자면 T는 U에 할당 가능 해야 한다.
따라서 다음과 같은 결과를 예상할 수 있다.
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // never
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "b" | "c" | "d"
하지만 실제 결과는 다음과 같다.
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
Q. 어째서 다음과 같이 결과가 나오는 걸까??
예제를 통해서 다음과 같은 과정을 이해하자.
type Diff<T, U> = T extends U ? never : T;
type Filter<T, U> = T extends U ? T : never;
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
제네릭 타입 위에서 조건부 타입은 유니온 타입을 만나게 되면 분산적으로 동작합니다. 이를 분산적 조건부 타입
이라고 합니다.
분산 조건부 타입은 타입을 인스턴스화하는 중에 자동으로 유니언 타입으로 분산됩니다.
예를들어, T에 대한 타입 인수 "a" | "b" | "c" | "d"
를 사용하여 T extends U ? X : Y
를 인스턴스화하면
("a" extends U ? X : Y)
| ("b" extends U ? X : Y)
| ("c" extends U ? X : Y)
| ("d" extends U ? X : Y)
로 결정되게 됩니다.
Diff
타입 결과를 다시 한 번 도출해보자.
type Diff<T, U> = T extends U ? never : T;
// 1. 첫 번째 과정
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
// 2. 두 번째 과정
type T30 =
| ("a" extends "a" | "c" | "f" ? never : T)
| ("b" extends "a" | "c" | "f" ? never : T)
| ("c" extends "a" | "c" | "f" ? never : T)
| ("d" extends "a" | "c" | "f" ? never : T);
// 3. 최종 결과
type T30 = "b" | "d"
주의사항
분산 조건부 타입은 제너릭 T에 유니온 타입을 넣을 때만 발생한다. 유티온 타입을 제너릭이 아니라 직접 리터럴로 넣게 되면 분산이 일어나지 않는다.type T1 = (1 | 3 | 5 | 7) extends number ? 'yes' : 'no'; // 'yes'
조건부 타입의
extends
절 안 에서 추론될타입 변수(infer)
를 도입이 가능하다.// 배열의 첫번째 요소를 찾는 타입 type arr1 = [3, 2, 1]; type FirstArrayElement<T extends unknown[]> = T extends [infer First, ...infer rest] ? First : never; const correctValue: FirstArrayElement<arr1> = 3; const wrongValue: FirstArrayElement<arr1> = 2; // type error not 2 -> is 3
조건부 타입을 적용할 때 infer 속성
을 사용하면 가독성 측면에서 훨씬 도움될 것 같다.
덕분에 extends 알게 되었습니다. 감사합니다 푸만능