타입스크립트도 조건문을 쓸 수 있다??!!? ㄴ(°0°)ㄱ

황준승·2023년 7월 2일
63
post-thumbnail

위 글 은 타입스크립트의 조건부 타입 을 사용해보고 정리하는 글입니다.

📌 조건부 타입

정의

조건부 타입(conditional type)은 입력된 제네릭 타입(T)에 따라 타입을 결정하는 것을 뜻한다.

T extends U ? X : Y

위의 예시에서 보는 것처럼 타입스크립트는 삼항연산자를 사용하여 값 대신 타입을 조건에 따라 결정한다.

타입스크립트의 extends

타입스크립트에서 사용하는 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'

📌 infer

정의

조건부 타입의 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 속성을 사용하면 가독성 측면에서 훨씬 도움될 것 같다.


참고자료

인파님 블로그 조건부 타입 완벽 이해하기
타입스크립트 핸드북 - 조건부 타입

profile
다른 사람들이 이해하기 쉽게 기록하고 공유하자!!

9개의 댓글

comment-user-thumbnail
2023년 7월 2일

덕분에 extends 알게 되었습니다. 감사합니다 푸만능

1개의 답글
comment-user-thumbnail
2023년 7월 2일

정리 깔끔해서 도움이 많이 되었습니다

1개의 답글
comment-user-thumbnail
2023년 7월 2일

시원해이~ 만푸~

1개의 답글
comment-user-thumbnail
2023년 7월 2일

분산의 역할이 있었군요!
잘 봤습니다 ㅎㅎ

1개의 답글
comment-user-thumbnail
2023년 7월 6일

너무 신기해서 하루만에 다 읽기엔 너무 좋아요 mini crossword

답글 달기