Notice
Recent Posts
Recent Comments
Link
관리 메뉴

윤일무이

[TypeScript] 조건부 타입 본문

❔ TypeScript/💭 한 입 크기 TypeScript

[TypeScript] 조건부 타입

썸머몽 2023. 12. 30. 01:53
728x90

조건부 타입

  • 삼항연산자를 이용해 조건에 따라 타입을 결정한다.
type A = number extends string ? string : number; // type A = number

type ObjA = {
  a: number;
};

type ObjB = {
  a: number;
  b: number;
};

type B = ObjB extends ObjA ? number : string; // true (ObjA = 슈퍼)

제네릭과 조건부 타입

function removeSpaces(text: string | undefined | null) {
  if (typeof text === 'string') {
    return text.replaceAll(' ', '');
  } else {
    return undefined;
  }
}

let result = removeSpaces('hi im www');
// result.toUpperCase(); // result?. 로 쓰거나 위 코드에 as string으로 타입 단언 해줘야함
console.log(result);
  • text의 타입을 string, undefined, null 중 하나라고 하면 함수 내부에서는 타입을 좁혔기 때문에 에러가 발생하지 않지만, 함수 바깥인 result에 string 관련 메서드를 적용하면 에러가 발생한다.
  • 이 때 result가 반드시 있다고 .? 연산자를 써주거나, 매개변수를 전달하는 코드에 string 타입이라고 result의 타입을 단언해줘야만 한다.
  • 또는 제네릭을 사용하는 방법도 있다.
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 www');
result.toUpperCase(); 

let result2 = removeSpaces(undefined); // undefined
  • 매개변수 text의 타입을 타입 변수 T로 지정해서 해당 타입에 조건을 걸어준다.
  • 하지만 함수 내부에서는 T가 뭔지 알 수 없다. (unknown으로 인지되기 때문)
  • 따라서 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 www');
result.toUpperCase(); 

let result2 = removeSpaces(undefined); // undefined
  • 하지만 return문에서 무조건 string, undefined로 받기로 한건데 무조건 any로 들어가게 돼서 검사가 안된다.
  • 따라서 함수 오버로딩을 사용하면 위와 같은 문제도 해결된다.
function removeSpaces<T>(text: T): T extends string ? string : undefined;
function removeSpaces(text: any) {
  if (typeof text === 'string') {
    return text.replaceAll(' ', '');
  } else {
    return undefined;
  }
}
  • 오버로드 시그니처를 먼저 적어주면, 어처피 함수 구현부는 오버로드 시그니처를 따라가기 때문에 타입을 지정하지 않아도 된다.
  • 매개 변수의 타입만 정의해주면 구현 시그니처 내부에서 조건부 타입을 추론할 수 있게 된다!

분산적인 조건부 타입

  • 조건부 타입을 유니온과 같이 사용할 때 분산적으로 사용할 수 있도록 하는 문법
type StringNumberSwitch<T> = T extends number ? string : number;

let a: StringNumberSwitch<number>; // string
let b: StringNumberSwitch<string>; // number
let c: StringNumberSwitch<number | string>; // string | number 
let d: StringNumberSwitch<boolean | number | string>; // string | number
  • 변수 d의 경우 number | string | number로 중복되기 때문에 다시 string | number로 타입이 추론된다.
  • 유니온으로 들어온 타입이 하나씩 조건부로 들어간다고 이해할 수 있다.
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<number | string | boolean, string>; 
// never(공집합)는 사라짐
// number | never | boolean => number | boolean

type Extract<T, U> = T extends U ? T : never;
type B = Extract<number | string | boolean, string>;
// never(공집합)는 사라짐
// never | string | never => string

infer (Inference, 추론)

type FuncA = () => string;
type FuncB = () => number;

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

type A = ReturnType<FuncA>; // string
type B = ReturnType<FuncB>; // never
  • 반환값의 타입을 가져와 확인하는 ReturnType을 만들었는데, FuncB의 경우 제대로 number가 나오지 않는다. (당연함)
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
  • 이 때 string 타입 부분을 지우고 infer R ? R 로 바꾸면 의도했던대로 반환값의 타입이 추론된다.

  • 원리는 () => infer R로, ()가 R의 서브 타입인지 확인하려면 들어갈 수 있는 타입을 추론하는 것 (같은 것끼리 서로 서브, 슈퍼로 간주)
type ReturnType<T> = T extends () => infer R ? R : never;

type C = ReturnType<number>; // never
  • 그런데 변수 C의 경우 number가 아니라 never가 나온다.
  • ReturnType<T> 타입은 주어진 함수 타입 T에서 함수의 반환값 타입을 추론하는 역할을 한다.
  • 그러나 들어온 number는 함수 타입이 아니기 때문에 R을 추론할 수가 없고, 이 때는 false라고 이해해 never가 나오게 된다.
  • 즉 T extends () => infer R 조건의 거짓이 되는 것이다.
// type PromiseUnpack<T> = any;
type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;

// T는 Promise 타입이어야 한다.
// Promise 타입의 결과값 타입을 반환해야 한다.

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

type PromiseB = PromiseUnpack<Promise<string>>;
// string;
  • Promise 타입의 결과값 타입을 반환하려고 한다.
  • A는 number, B는 string이 잘 나오면 된다.
  • 먼저 PromiseUnpack 타입을 만들고 타입 변수 T를 넣은 후 any라고 한다.
    • 이 때 매개변수로 들어오는 T는 Promise여야 한다.
  • 따라서 any를 지우고, 들어온 T(Promise)는 Promise<infer R> 일때 R이라고 적어준다.
    • 이 Promise 타입의 결과값을 infer하는데 true가 되기 위해 number, string으로 추론된다.

 

**출처: 한 입 크기로 잘라먹는 타입스크립트 (인프런, 이정환 강사님)

728x90