https://2sa7uk5m8eundh3t.public.blob.vercel-storage.com/images/infer-64cl5pQYW4UdtxjkEpNhfuIQ2a6EGU.png

Advanced TypeScript | January 23, 2023

TypeScript 의 Infer 에 대하여

객체나 배열과 같은 복잡한 타입에서 어떻게 특정한 부분의 타입만을 추론할 수 있는가?

Infer...?

infer 는 조건부 타입에서 조건식이 참으로 평가될 때, 비교식에서 대응되는 특정 타입을 추론해 내는 기능입니다.

🤷🏻‍♂️: 아니 이게 뭔 뚱딴지 같은 소립니까? 조건부 타입은 또 뭔데요?

라고 생각하실 수 있는 여러분을 위해, 조건부 타입에 대해 간단히 알아 본 후 예제와 함께 Infer 에 대해 알아 봅시다.

Conditional Types

조건부 타입은 특정 타입이 비교 대상 타입에 할당 될 수 있는지 비교하고, 참/거짓 분기로 타입을 반환할 수 있습니다.

Typescript

1T extends U ? X : Y
조건부 타입의 기본 형태입니다.

기본 형태는 위와 같습니다. 특정 타입(T)이 비교 대상 타입(U)에 할당될 수 있는지 비교하고 참일 경우 X 타입을, 거짓일 경우 Y 타입을 반환합니다.

실제 사용 케이스를 예로 들어봅시다. 만약 어떤 타입이 올지 모르는 제네릭 타입이 배열 타입에 할당될 수 없다면, never 타입을 반환하는 타입을 어떻게 구현할 수 있을까요?

Typescript

1type IsArray<T> = T extends unknown[] ? T : never;

위 처럼 조건부 타입을 사용해서, 제네릭 타입이 배열 타입에 할당될 수 있는지 비교하고, 할당될 수 있으면 그 타입을 반환하고, 그렇지 않은 경우 never 타입을 반환하도록 구현하면 됩니다.

Typescript

1type A = IsArray<number[]>; // 배열 타입에 할당 가능하므로 number[]
2type B = IsArray<number>; // 배열 타입에 할당할 수 없으므로 never

결과는 아시다시피 위와 같습니다.

오예. 우리는 이제 조건부 타입을 어느정도 알게되었습니다.

여기서 억지스럽지만 한 가지 요구사항을 바꿔봅시다. 만약 배열 타입에 할당 가능할 경우 배열의 요소에 대한 타입을 반환할 수는 없을까요?

Typescript

1type NumberArrayElement<T> = T extends number[] ? number : never;

number[] 타입일 경우에 number 를 반환하도록 구현해 봤습니다. 사실상 number[] 타입만이 할당될 수 있으므로 아닌 경우 무조건 never 를 반환할 것입니다.

🤷🏻‍♂️: 아니 그러면 다른 배열 요소 타입을 반환하고 싶으면 어떡하란 말입니까?

그럴 때 우리는 infer를 사용할 수 있습니다.

Infer!

자, 조건부 타입까지는 알았으니 예제를 통해 infer 에 대해 알아봅시다.

Typescript

1T extends infer U ? U : T;

기본적인 사용법은 위와 같습니다. 특정 타입(T)이 비교 대상 타입(U)에 할당될 수 있는지 비교하고 참일 경우 비교 대상 타입(U)을 반환하고, 그렇지 않으면 특정 타입(T)을 반환합니다.

제가 조건부 타입에 대해 말씀드린 이유가 있습니다. infer 는 조건부 타입 구문에서만 사용할 수 있습니다. 또한 조건이 참일 경우 반드시 infer 를 통해 생성한 타입을 포함하고 있는 타입을 반환해야 합니다.

어라 그런데 생긴게 조건부 타입과 별반 다를 게 없어 보이죠? 사실 위의 예제처럼 사용하면 아무런 의미가 없습니다. 설명을 조금 더 드린 뒤 실제 사용 케이스와 함께 보시죠.

Infer 는 타입을 추론한다?

infer 는 조건부 타입 구문에서 비교식이 참인 경우, 비교식에서 대응되는 타입을 추론해 냅니다.

Typescript

1T extends infer U ? U : T;

기본적인 사용법으로 보여드린 위의 예제에서의 비교식이 참인 경우, U 타입은 T 타입과 대응합니다. 따라서 U 타입은 자연스레 T 타입으로 추론이 가능한 것이죠.

아직 잘 감이 오지 않죠? 실제 사용 케이스를 예시로 들어봅시다.

아까 조건부 타입에 대한 예시 설명의 마지막 내용에서 이런 의문을 가졌었죠?

🤷🏻‍♂️: 아니 그러면 다른 배열 요소 타입을 반환하고 싶으면 어떡하란 말입니까?

특정 타입이 비교 대상 타입에 할당될 수 있으면 비교식에서 대응되는 배열의 요소에 대해 추론된 타입을 반환 하려면 어떻게 타입을 구현해야 할까요?

Typescript

1type ArrayElement<T> = T extends (infer U)[] ? U : never;

위와 같이 구현할 수 있겠습니다. 비교식이 참이라면 T 타입은 배열 타입일 테니, 비교식에서 infer U 에 대응되는 부분은 배열의 요소이므로 T 타입의 배열 요소에 해당하는 추론된 U 타입을 반환하도록 하면 되겠네요!

Typescript

1type A = ArrayElement<number[]>; // number
2type B = ArrayElement<string[]>; // string
3type C = ArrayElement<boolean>; // never

결과는 다음과 같습니다. C 타입의 경우 boolean 타입은 배열 타입에 할당될 수 없으므로 위의 조건부 타입에 의해 never 타입이 반환됩니다.

Infer 가 추론하고자 하는 타입이 여러 개라면?

infer 는 비교식에 대응되는 타입을 추론한다는 사실을 알았습니다. 그럼 만약 infer 를 사용한 동일한 추론 가능한 타입을 중복하여 사용한다면, 즉 추론하고자 하는 타입이 여러 개라면 어떨까요?

Typescript

1type UserProperties<T> = T extends { id: infer U, name: infer U } ? U : never;

Typescript

1type User = { id: string, name: number };
2
3type A = UserProperties<User>;

위의 예제의 UserProperties 타입에서 T 객체 타입의 id 와 name 프로퍼티에 대응되는 추론 가능한 타입이 여러 개인 경우, 추론 가능한 모든 타입의 합집합인, 즉 유니온 타입이 반환됩니다.

Typescript

1type A = UserProperties<User>; // string | number

Examples

자, 이제 infer 에 대해 알았습니다. 완벽한 이해를 위해 예제를 좀 더 볼까요?

TypeScript 에는 이미 구현된 많은 유틸리티 타입들이 존재합니다. 이 유틸리티 타입들은 infer 를 사용하여 구현하기도 합니다.

본 글의 주제인 infer 를 사용하여 만든 유틸리티 타입을 한번 봅시다.

Typescript

1type A = Parameters<(x: number) => string>; // [x: number]
2type B = ReturnType<(x: number) => string>; // string

Parameters 타입은 함수 타입을 전달하면 함수 타입의 파라미터에 대응되는 추론된 타입을 반환하고, ReturnType 타입은 반환 값에 대응되는 추론된 타입을 반환합니다.

오우. 이제는 어떻게 구현되어 있을지 예상이 갑니다.

Typescript

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

T 타입이 함수 타입에 할당할 수 있으면, T 타입은 함수 타입입니다. 조건식에서 대응되어야 할 부분은 함수의 파라미터 타입이므로, 파라미터 타입과 반환 값의 타입에 infer 를 사용해 추론할 P 와 R 타입이 대응되도록 하고, 참일 경우 반환하도록 합니다.