TypeScript - infer

DatQueue·2022년 9월 14일
33
post-thumbnail

소개

조건부 타입의 조건식이 참으로 평가될 때에는 infer키워드를 사용할 수 있다.

예를 들어,

Element<number> extends Element<infer U>와 같은 타입을 작성하면, U타입은 number타입으로 추론(infer)된다. 이후, 참인 경우에 대응되는 식에서 추론한 U타입을 사용할 수 있다.

간단히 말하면 아래와 같이 정의할 수 있다.

U 가 추론 가능한 타입이면 참, 아니면 거짓

해당 “infer”에 관한 페이지를 “Conditional Type” (조건부 타입) 페이지의 하위 페이지로 둔 것엔 이유가 있다.

infer 키워드는 제약 조건 extends가 아닌 조건부 타입 extends절에서만 사용 가능하다.

즉, “조건부 타입” 절을 벗어나면 그다지 유용한 키워드가 아닐지도 모른다. 하지만, 해당 조건부 타입 절에서만큼은 굉장히(?) 유용한 키워드고 자주 등장하는 단골 구문이다.

기본 구조

T extends infer U ? X : Y

(설명은 위를 참조)

예제를 통해 알아보는 infer의 사용


예제 1) __ 간단한 예제

type MyType<T> = T extends infer R ? R : null;

const a : MyType<number> = 123;
console.log(typeof a); //number

타입 변수 RMyType<number>에서 받은 타입 number가 되고, infer 키워드를 통해 타입 추론이 가능하게 된다.

위 코드에서 number타입은 당연히 타입 추론이 가능하니 R을 반환하는 것이다. 어떠한 타입도 추론이 되지 않는다면 null을 반환하게 된다. 콘솔을 통해 변수 a의 타입을 프린트해보면 number를 반환하고 123이란 숫자를 대입할 수 있다.

그런데 위의 코드의 결과를 얻기 위해, 즉 변수 a의 타입을 얻기 위해 , 굳이 infer키워드를 사용해야할까?

type MyType<T> = T extends number ? number : null;

const a : MyType<number> = 123;
console.log(typeof a); //number

그냥 위와 같이 number를 바로 명시해 주는 것이 편할 것이다. 뭐 사실 이러한 짧은, 아무 의미없는 코드는 그냥 아래와 같이 제네릭도 사용하지 않고 바로 타입을 명시하는 것이 빠를 것이다.

type MyType = number ;

const a : MyType = 123;
console.log(typeof a); //number

하지만 다음 경우부턴 조금 더 “유용”해진 infer를 만날 수 있을 것이다.


예제 2) __ 조금 복잡하지만 유용한 예제

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function fn(num : number) {
  return num.toString(); 
}

const a : ReturnType<typeof fn> = "Hello";   // ReturnType<T> 이용
console.log(a); //Hello

위 코드만 보고 “ 뭐가 유용하지 ? ” 라고 생각할 수 있다. 이 코드를 처음보고 느꼈던 생각은 유용하긴 커녕 가장 위의 코드 줄을 이해하는데 머리만 아팠다.

하지만, 가장 위의 코드 줄(type ReturnType<> ~~) 없이도 나머지 코드들이 실행되는데 문제가 없다면 어떨까?

그렇다. 위의 ReturnType<T>유틸리티 타입이다.

유틸리티 타입에 대해선 따로 다루고 있으므로 여기서 길게 작성하진 않겠다. 아주 간단히 말하자면 TypeScript에선 공통 타입 반환을 용이하게 하기 위해서 전역 타입으로 사용할 수 있는 유틸리티 타입을 제공한다.

그리고 해당 ReturnType<T>는 함수 T의 반환 타입으로 구성된 타입을 만든다.

간단히 알아보자면 다음과 같다.

declare function f1() : {
  a : string,
  b : number,
}

type T0 = ReturnType<() => string>;  // string
type T1 = ReturnType<(s : string) => void>;  // void
type T2 = ReturnType<typeof f1>; // {a : string , b : number}

이렇게 우린 유틸리티 타입을 사용하게 되면 전역으로 타입이 작용하므로 따로 해당 타입 (여기선 ReturnType)에 관해 따로 명시해 줄 필요가 없다.

즉, 아래와 같이 코드를 작성해도 ReturnType<T>은 유틸리티 함수이므로 원활히 타입을 얻을 수 있다.

function fn(num : number) {
  return num.toString(); 
}

const a : ReturnType<typeof fn> = "Hello"; // ReturnType<T> --> Utility Types
console.log(a); //Hello

vsCode의 기능을 활용하여 해당 ReturnType에 마우스를 오버하여 “정의” 를 확인해보자.

앞서 우리가 처음에 명시해 준 아래 코드가

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

불러옴과 동시에 내장이 되어있는 것을 확인할 수 있다.

물론, 지금 유틸리티 타입에 관해서 깊게 알아보자는 것이 아니다.

핵심은 바로 infer키워드를 이용하여 유틸리티 타입으로 만들었다는 것.

즉, ReturnTypeinfer키워드를 사용하여 만들어졌다는 것이다 !!!

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

해당 구문을 파고들어 볼 필요가 있다. 우린 지금 유틸리티 타입의 장점에 대해 말하고자 하는것이 아니기 때문이다.

infer 키워드가 “어떻게” ReturnType을 유틸리티 타입으로 만들었는지가 핵심이다.

만약 코드를 아래와 같이 작성한다면 어떨까?

타입을 직접적으로 바로 명시해버리는 것이다.

type ReturnType<T extends (...args : any) => any> = string;   // "string"으로 바로 명시

function fn(num : number) {
  return num.toString(); 
}

const a : ReturnType<typeof fn> = "Hello";
console.log(a); //Hello

위 코드에 어떠한 오류도 없다.

변수 a의 값으로 string타입인 “Hello”를 기입함에 따라 위에서 명시한 ReturnType<T>의 타입인 “string”에 할당할 수 있기 때문이다.

그런데 함수(fn)의 리턴타입을 반환하는 타입을 만드는 ReturnType에서 위와 같이 “직접적”으로 기입하는 것이 과연 옳을까?

지금이야 함수 fn의 리턴 값이 toString()에 의해 “string” 타입이 되었으니까 코드가 문제 없이 진행되었지만, 만약 fn의 리턴 값이 number혹은 boolean등과 같은 다른 타입일 경우 어떨까?

우린 위의 직접적으로 명시하였던 “string” 타입을 그에 맞게 (함수의 리턴값에 맞게) 수정해주어야 할 것이다.

혹은, 아래와 같이 유니온 타입으로 명시해 줘야 한다.

type ReturnType<T extends (...args : any) => any> = string | number;  // 유니온 타입

function fn(num : number) {
  return num; 
}

const a : ReturnType<typeof fn> = 6;
console.log(a); // 6

굉장히 비효율 적이고 의미없는 타입명시라고 할 수 있다. 함수의 반환 값을 수정하고 싶을 때마다 타입을 일일이 수정해줘야하는 꼴이다.

우리가 원하는 ReturnType<T>의 취지에서 벗어난다.

그래서 우린 infer를 소환시키게 되는 것이다 !!!

type ReturnType<T extends (...args : any) => any> = T extends (...args : any) => infer R ? R : any;

function fn(num : number) {
  return num.toString(); 
}

const a : ReturnType<typeof fn> = "Hello";

바로 타입을 정의하는 것이 아닌, infer를 통해 타입을 추론 시키는 것이다.

typeof fn은 타입 매개변수 T가 되는 것이고 해당 TR이 됨을 모두 알 것이다.

이때, 함수 fn의 리턴 값이 “string” 타입이므로 R또한 “string”이 되는 것이고 결국 최종 ReturnType<typeof fn>infer R ? R : any에 의해 R 즉, “string”이 되는 것이다.

이렇게 우린, 타입을 따로 수정할 필요없이 오직 “추론”에 의해서 함수의 반환 타입에 의한 타입을 만들 수 있게 된다.


Promise 객체안의 타입 꺼내기

Promise 객체 안에 있는 값의 타입을 편하게 꺼내려 할 경우 infer키워드를 사용하면 “런타임에서 결정되는 타입”을 손쉽게 정의할 수 있다.

위의 “런타임에서 결정되는 타입 ” 이란 구문에 관해 자세히 알고 싶다면 아래 포스팅 참조

⬇⬇⬇

타입스크립트의 점진적 타이핑

코드를 통해 알아보자.

ex 1)

type UnpackPromiseArray<P> = P extends Promise<infer K>[] ? K : any

const arr = [Promise.resolve(true)];

type ExpectedBoolean = UnpackPromiseArray<typeof arr> // boolean

ex 2)

type PromiseType<T> = T extends Promise<infer U> ? U : never;

type A = PromiseType<Promise<number>>; // number

type B = PromiseType<Promise<string | boolean>>; // string | boolean

type C = PromiseType<number>; // never

Tuple 다루기

해당 내용에 앞서 타입스크립트의 Tuple에 관한 기초를 원한다면 아래 포스팅을 먼저 참조.

⬇⬇⬇

타입스크립트 - 튜플

[string, number, boolean]과 같은 TypeScript의 Tuple Type에서 그 꼬리 부분인 [number, boolean]과 같은 부분만 가져오고 싶은 상황을 생각해보자.

Conditional Type과 Variadic Tuple Type을 활용함으로써 이를 간단히 구현할 수 있다.

type TailOf<T> = T extends [unknown, ...infer U] ? U : [];

// type A = [boolean, number];
type A = TailOf<[string, boolean, number]>;

const a: A = [true, 123]; // assignable !
const a1: A = ["hello", 123]; // Error -- not assignable!

첫 요소(여기선 string)를 제외하고 ...infer U구문을 이용하여 뒤의 요소(boolean, number)들을 모두 선택한 것을 확인할 수 있다.

위의 구문을 “함수”를 이용해서 나타낼 수도 있다. 아래와 같이 작성한다. 해당 구문에선 infer을 이용한 “Conditional Type”은 썩 유용해 보이진 않는다.

function tailOf<T extends unknown[]>(arr: readonly[unknown, ...T]) {
  const [_ignored, ...rest] = arr;
  return rest;
}

type A = [string, boolean, number];

const myTailOf: A = ["hello", true, 123];
const testTailOf = tailOf(myTailOf);
console.log(testTailOf); //[true, 123]

생각정리

이번 포스팅에선 "infer"이라는 키워드를 주제로 알아보았다. 해당 "infer"은 단어의 뜻에서도 알 수 있듯이 타입의 "추론"을 가능케 해준다. 즉, 컴파일 과정에서 타입을 미리 명시해주지 않아도, 혹은 그러한 경우가 효율적이지 못할경우 우린 infer 을 이용해서 런타임 과정에서 타입을 제시해 컴파일러가 추론케한다.

이것은 위에서 잠깐 언급하였듯이 타입스크립트의 "점진적 타이핑"관점에서 굉장히 의미있는 특징이라 할 수 있다. 또한 우린 "infer"을 익힘으로써 타입스크립트의 "Conditional Type(조건부 타입)"을 이해할 수 있다. 많이 보게 될 키워드인만큼 깊이 생각할 필요가 있다 본다.

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

2개의 댓글

comment-user-thumbnail
2023년 8월 2일

좋은 내용이네요, 다소 어렵지만 재미난 주제였습니다. 여러번 읽어봐야할듯 ㅎㅎ

1개의 답글