조건부 타입의 조건식이 참으로 평가될 때에는 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
의 사용type MyType<T> = T extends infer R ? R : null;
const a : MyType<number> = 123;
console.log(typeof a); //number
타입 변수 R
은 MyType<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
를 만날 수 있을 것이다.
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
키워드를 이용하여 유틸리티 타입으로 만들었다는 것.
즉, ReturnType
은 infer
키워드를 사용하여 만들어졌다는 것이다 !!!
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
가 되는 것이고 해당 T
는 R
이 됨을 모두 알 것이다.
이때, 함수 fn
의 리턴 값이 “string” 타입이므로 R
또한 “string”이 되는 것이고 결국 최종 ReturnType<typeof fn>
은 infer R ? R : any
에 의해 R
즉, “string”이 되는 것이다.
이렇게 우린, 타입을 따로 수정할 필요없이 오직 “추론”에 의해서 함수의 반환 타입에 의한 타입을 만들 수 있게 된다.
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
에 관한 기초를 원한다면 아래 포스팅을 먼저 참조.
⬇⬇⬇
[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(조건부 타입)"을 이해할 수 있다. 많이 보게 될 키워드인만큼 깊이 생각할 필요가 있다 본다.
좋은 내용이네요, 다소 어렵지만 재미난 주제였습니다. 여러번 읽어봐야할듯 ㅎㅎ