본문 바로가기

온라인 강의(유데미, 인프런 등)/한 입 크기로 잘라먹는 타입스크립트(인프런)

[한 입 크기로 잘라먹는 타입스크립트] 조건부 타입

반응형

1. 조건부 타입

조건부 타입은 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

이번에는 실용적인 예제를 살펴보자. 다음은 파라미터로 String 타입의 값을 제공받아 공백을 제거한 다음 반환하는 함수이다.

function removeSpaces(text: string) {
  return text.replaceAll(" ", "");
}

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

따라서 이럴 때에는 타입 단언보다는 함수 오버로딩을 이용하는게 더 좋다. 오버로드 시그니쳐의 조건부 타입은 구현 시그니쳐 내부에서 추론이 가능하다. 따라서 다음과 같이 오버로드 시그니쳐를 추가해 함수 오버로딩을 구현한다.

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("hi im winterlood");
// string

let result2 = removeSpaces(undefined);
// undefined

 

2. 분산적인 조건부 타입

타입 변수에 Union 타입을 할당해보자.

type StringNumberSwitch<T> = T extends number ? string : number;

(...)

let c: StringNumberSwitch<number | string>;
// string | number

지금까지 배운 조건부 타입 문법에 따라 변수 c의 타입은 number | string은 number의 서브타입이 아니므로 조건식이 거짓이 되어 number가 될거라고 예상할 수 있다. 그러나 변수 c는 string | number 타입으로 정의된다.

그 이유는 조건부 타입의 타입 변수에 Union 타입을 할당하면 분산적인 조건부 타입으로 조건부 타입이 업그레이드 되기 때문이다.

분산적인 조건부 타입은 다음과 같이 동작한다.

타입 변수에 할당한 Union 타입 내부의 모든 타입이 분리된다. 따라서 StringNuberSwitch<number | string> 타입은 다음과 같이 분산된다.

  • StringNumberSwitch<number>
  • StringNumberSwitch<string>

그리고 다음으로 분산된 각 타입의 결과를 모아 다시 Union 타입으로 묶는다.

  • 결과 : number | string

# Exclude 조건부 타입 구현하기

type Exclude<T, U> = T extends U ? never : T;

type A = Exclude<number | string | boolean, string>;

// 과정

1. Union 타입이 분리된다.
Exclude<number, string>
Exclude<string, string>
Exclude<boolean, string>
2. 각 분리된 타입을 모두 계산한다.
T = number, U = string 일 때 number extends string 은 거짓이므로 결과는 number
T = string, U = string 일 때 string extends string 은 참이므로 결과는 never
T = boolean, U = string 일 때 boolean extends string 은 거짓이므로 결과는 boolean
3. 계산된 타입들을 모두 Union으로 묶는다
결과 :  number | never | boolean

계산 결과 타입 A는 number | never | boolean 타입으로 정의된다.
그런데 여기서 공집합을 의미하는 never 타입은 Union으로 묶일 경우 사라진다. 
그 이유는 공집합과 어떤 집합의 합집합은 그냥 원본 집합이 되기 때문이다.
따라서 최종적으로 타입 A는 number | boolean 타입이 된다.

 

3. infer

infer는 조건부 타입 내에서 특정 타입을 추론하는 문법이다.

infer는 다음과 같이 특정 함수 타입에서 반환값의 타입만 추출하는 특수한 조건부 타입인 ReturnType을 만들 때 이용할 수 있다.

type ReturnType<T> = T extends () => infer R ? R : never;

type FuncA = () => string;

type FuncB = () => number;

type A = ReturnType<FuncA>;
// string

type B = ReturnType<FuncB>;
// number

조건식 T extends () => infer R에서 infer R은 이 조건식을 참이 되도록 만들 수 있는 최적의 R 타입을 추론하라는 의미이다. 따라서 A 타입을 계산할 때의 위 코드의 흐름은 다음과 같다.

  1. 타입 변수 T에 함수 타입 FuncA가 할당된다.
  2. T는 () ⇒ string 이 된다.
  3. 조건부 타입의 조건식은 다음 형태가 된다. () ⇒ string extends () ⇒ infer R ? R : never
  4. 조건식을 참으로 만드는 R 타입을 추론한다. 그 결과 R은 string이 된다.
  5. 추론이 가능하면 이 조건식을 참으로 판단한다. 따라서 결과는 string이 된다.
만약 다음과 같이 추론이 불가능하다면 조건식을 거짓으로 판단한다.
type ReturnType<T> = T extends () => infer R ? R : never;

type FuncA = () => string;

type FuncB = () => number;

type A = ReturnType<FuncA>;
// string

type B = ReturnType<FuncB>;
// number

type C = ReturnType<number>;
// 조건식을 만족하는 R추론 불가능
// never

다음은 Promise의 resolve 타입을 infer를 이용해 추출하는 예 이다.

type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;
// 1. T는 프로미스 타입이어야 한다.
// 2. 프로미스 타입의 결과값 타입을 반환해야 한다.

type PromiseA = PromiseUnpack<Promise<number>>;
// number

type PromiseB = PromiseUnpack<Promise<string>>;
// string

 

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8/dashboard

 

한 입 크기로 잘라먹는 타입스크립트 - 인프런 | 강의

문법을 넘어 동작 원리와 개념 이해까지 배워도 배워도 헷갈리는 타입스크립트 이제 제대로 배워보세요! 여러분을 타입스크립트 마법사🧙🏻‍♀️로 만들어드립니다., - 강의 소개 | 인프런

www.inflearn.com

 

반응형