ghlee.dev
TypeScript의 Infer 키워드에 적응하기
2024. 5. 16.이번 글에서는 TypeScript의 infer
키워드에 대해서 살펴보고자 한다.
infer
키워드는 영문 뜻 그대로 타입을 명시적으로 추론할 때 사용하는데, 서론부터 힘빠지는 말이지만 구체적으로 어떤 상황에서 사용하면 좋을지 효율적인 유스 케이스를 아직 찾지 못하였다. 😬
개인적인 생각으로는 일반적인 응용 웹 애플리케이션 개발 시 infer
키워드를 많이 사용하게 되는 코드는 이미 구조적으로 문제가 있지 않을까 고민해 볼 필요가 있어 보인다. 왜냐하면 대부분의 문제는 TypeScript의 암묵적 추론으로 해결할 수 있기 때문이다. 다만 유틸리티 타입의 구현체 등 TypeScript 내부적으로 상당히 많이 사용되므로 알아두면 평소 타입 작성 시 이해가 훨씬 수월할 것이다.
이제부터 infer
키워드를 사용한 예제를 다섯 가지 살펴보면서 사용 방법에 대해 감을 잡는 시간을 가져보도록 하겠다.
제네릭과
extends
같은 조건부 타입,typeof
등 어느정도 TypeScript에 대해 알고 있어야 이해가 수월하다.
첫번째 예제
처음이니 간단한 예제부터 살펴보자.
배열 안에 있는 요소의 타입을 추론하는 코드이다. 이해를 위해서 조금씩 분해하며 살펴보자.
type InferedValueInArray<T> =
- 타입을 선언하고 제네릭으로
T
를 선언하였다.
- 타입을 선언하고 제네릭으로
T extends Array<infer V> ? V : unknown;
T
는 어떤 유형인지 모르는V
를 가지는 배열로 되어있는지 묻는 조건부 코드이다.- 조건부 코드는 삼항연산자로 이루어져 있는데 조건이 참이면
V
를 반환하고 거짓이면unknown
을 반환한다.
const examples = ['hello', 'world', '!'];
- 테스트를 위해 문자열 배열을 만들었다.
const example: InferedValueInArray<typeof examples> = examples[0];
text
변수를 만들어서InferedValueInArray
타입을 선언해주었다.- 제네릭으로
typeof examples
을 넣어주었는데examples
는 문자열 배열로 이루어져 있으니 자연스럽게string[]
을 의미하게된다. - 최종적으로
const example
는string
으로 추론된다.
여기서 재미있는 것은 textList
을 다양한 타입으로 만들면 유니온 타입으로 추론한다.
Typescript Playground에서
BigInt
를 사용하려면 TSConfig에서 Target을 ES2020 이후로 설정해줘야 한다.
두번째 예제
다음으로 볼 예제는 TypeScript에 내장되어있는 타입이다.
lib.es5.d.ts
Parameters
은 함수의 매개변수를 추론하는 유틸리티 타입으로 TypeScript의 lib.es5.d.ts
파일을 보면 실제 구현된 코드를 확인할 수 있다. 이번 예제도 순서대로 살펴보자.
type Parameters<T extends (...args: any) => any>
Parameters
타입을 선언하고 제네릭으로T
를 선언하였다.T
는 매개변수가any
이면서 반환 타입이any
인 함수를 받는다.
T extends (...args: infer P) => any ? P : never;
T
는 어떤 유형인지 모르는 매개변수P
와 반환 타입이any
인 함수인지 조건부 확인을 한다.- 사실상 모든 함수 형태를 의미한다.
- 참이면 매개변수
P
를 반환하며 거짓이면never
를 반환한다.
const add = (a: number, b: number) => a + b;
- 두 숫자를 더하는
add
함수를 선언해주었다.
- 두 숫자를 더하는
type ParamsOfAdd = Parameters<typeof add>;
ParamsOfAdd
타입을 선언하고 내장된Parameters
타입의 제네릭으로typeof add
를 넣어주었다.Parameters
는add
함수 타입을 이용하여 숫자a
와 숫자b
매개변수를 가진 튜플을 추론했다.[a: number, b: number]
const subtract: ParamsOfAdd = (a: number, b: number, c: number) => a - b - c;
- 새로운 함수
subtract
를 만들어서ParamsOfAdd
타입을 지정해주었지만 에러가 발생한다. subtract
함수는 숫자 매개변수인건 동일하지만 개수가 총 세개이므로 이전에 추론된 튜플 타입에 맞지 않는다.
- 새로운 함수
이제 infer
키워드에 대해 어느정도 감이 오는가? 아직 잘 모르겠다면 두번째 예제와 비슷한 수준의 예제를 한번 더 보자.
세번째 예제
이번에 살펴볼 예제는 ReturnType
이다. 함수의 반환 값을 추론하는 유틸리티 타입으로 Parameters
와 마찬가지로 TypeScript에 내장되어 있다.
lib.es5.d.ts
type ReturnType<T extends (...args: any) => any>
ReturnType
타입을 선언하고 제네릭으로T
를 선언하였다.T
는 매개변수가any
이면서 반환 타입이any
인 함수를 받는다.
T extends (...args: any) => infer R ? R : any;
T
는 매개변수any
와 어떤 유형인지 모르는 반환 타입R
인 함수인지 조건부 확인을 한다.- 사실상 모든 함수 형태를 의미한다.
T
가 함수 형태에 해당하면 그 함수의 반환 타입을 추론한R
을 반환하며 거짓이면any
타입을 반환한다.
const add = (a: number, b: number) => a + b;
- 두 숫자를 더하는 함수를 선언했다.
const addToString = (a: number, b:number) => ${a + b}
;`- 두 숫자를 더한 후 문자열로 변환하는 함수를 선언했다.
type ReturnTypeOfAdd = ReturnType<typeof add>
ReturnTypeOfAdd
타입을 선언하고ReturnType
의 제네릭으로typeof add
를 넣어주었다.ReturnTypeOfAdd
은number
로 추론된다.
type ReturnTypeOfAddToString = ReturnType<typeof addToString>
- 마찬가지로
ReturnTypeOfAddToString
타입을 선언하고ReturnType
의 제네릭으로typeof addToString
을 넣어주었다. ReturnTypeOfAddToString
은string
으로 추론된다.
- 마찬가지로
infer
을 사용할 때 주의할 점은 조건부 extends
에서 참에 해당할 때만 사용될 수 있는데, 헷갈릴 수 있지만 이는 잘 생각해 보면 당연하다. 애초에 infer
을 넣고 추론할 수 있는지 여부가 조건이기 때문에 추론이 불가능한 상황이라면, 즉 거짓이라면 infer
로 선언한 타입 변수를 사용할 수 없다.
네번째 예제
이번에 살펴볼 예제는 TypeHero 라고 하는 웹 사이트에서 가져온 문제이다. TypeScript 문제를 알고리듬 문제처럼 풀어보는 웹 사이트인데 정말 기상천외한 문제와 해결 방법을 만나볼 수 있으니 궁금하면 들어가 보길 권장한다.
주어진 문제는 배열이나 튜플 타입의 첫번째 요소를 타입으로 반환하는 문제이다. 실제로 웹 사이트에서 문제를 풀 때는 이미 선언되어있는 First
타입을 오류가 안나도록 수정해주면 된다.
해당 코드는 내가 문제 풀이한 코드로 이제까지의 설명 방식으로 살펴보겠다.
type First<T extends unknown[]>
First
타입을 선언하고 제네릭T
는unknown[]
로 제한하였다.- 제네릭으로 사실 상 배열만 넣을 수 있게 강제되었다.
T extends [infer F, ...unknown[]] ? F : never
- 조건부
extends
로 배열 안에infer
과 함께 Spread 문법으로...unknown[]
이 사용됐다. - 제네릭
T
가 주어진 배열에 첫번째 요소가 있다면F
변수로 추론한다는 의미로 이 조건이 참이면F
를 그대로 반환하고 거짓이면never
타입을 반환한다. - 여기서
never
타입은 추론할 수 없음을 의미한다고 보면 된다.
- 조건부
type arr1 = ['a', 'b', 'c']
- 테스트에 사용할 튜플 타입을 선언했다.
type arr2 = [3, 2, 1]
- 테스트에 사용할 튜플 타입을 선언했다.
type head1 = First<arr1>
head1
타입은 튜플의 첫번째 요소인"a"
로 추론된다.
type head2 = First<arr2>
- 마찬가지로
head2
타입은 튜플의 첫번째 요소인3
으로 추론된다.
- 마찬가지로
이번 예제는 infer
을 배열 안에서 사용했는데 이 방식을 활용하여 배열 타입의 요소를 다양하게 추출할 수 있다.
위 코드처럼 배열 타입의 두번째 요소까지 추론하여 첫번째와 두번째 요소를 유니온 타입으로 반환해볼 수도 있다.
다섯번째 예제
타입을 정의할 때도 재귀가 가능한 것을 알고 있는가? 아는 사람이 많지 않을 거라 생각되는데 infer
을 활용하면 재귀를 구현할 수 있다.
이번 예제는 조금 어려울 수 있으니 마음의 준비를 하고 보자. 😬
es2019.array.d.ts
ECMAScript 2019에는 Array.flat
함수가 새롭게 나왔는데 MDN의 설명을 보면 다음과 같다.
The flat() method of Array instances creates a new array with all sub-array elements concatenated into it recursively up to the specified depth.
recursively
, 즉 내부적으로 재귀를 사용하여 구현되어 있음을 알 수 있는데 이는 타입 정의에서도 마찬가지이다.
TypeScript에서는 이에 대한 타입을 es2019.array.d.ts
파일에 정의하였다. 이 코드에서 우리가 자세히 볼 것은 FlatArray
타입으로 flat
함수의 반환 타입으로 사용하고 있다.
굉장히 난해하게 보이지만 이는 하드코딩 되어있는 정수 배열때문에 보기 힘들어서 그럴 수 있으니(?) 이 부분을 임의로 생략하고 구조 분해해서 분석해보자.
type FlatArray<Arr, Depth extends number> =
FlatArray
타입을 선언하고 제네릭으로Arr
와Depth
를 받는다.Depth
는number
타입으로 제한하였다.
{ done: Arr; recur: ...; }
- Object의 Key 중 하나로
done
은 제네릭으로 받은Arr
를 값으로 갖는다.
- Object의 Key 중 하나로
[Depth extends -1 ? "done" : "recur"];
- 제네릭
Depth
의 값이-1
이면 Object의done
의 값을 반환하고 그렇지 않으면recur
값을 반환한다.
- 제네릭
크게보면 FlatArray
는 객체의 값 중 하나를 반환하는 타입으로 볼 수 있다. 이제 아직 다루지 않은 recur
의 값을 살펴보자.
하드코딩된 정수 배열은 임의로
[-1 ~ 20]
로 표현하였다.
Arr extends ReadonlyArray<infer InnerArr> ?
- 제네릭
Arr
가ReadonlyArray
의 추론한 요소인InnerArr
인지 확인하는 조건문이다.flat
함수는 배열의 배열을 펼치는 함수이기 때문에 배열의 요소의 변수 명이InnerArr (안쪽 배열)
이다.
ReadonlyArray
는 말 그대로 읽기 전용 배열 타입으로 보면 된다. 구현된 코드는es2019.array.d.ts
로 가면 자세히 볼 수 있으며 여기서는 생략한다.
- 제네릭
FlatArray<InnerArr, [-1 ~ 20][Depth]> : Arr;
- 조건문이 참이면 재귀적으로
FlatArray
를 반환한다.- 제네릭
Arr
에는 아까 추론했던 내부 요소인InnerArr
를 새로 넣어준다. - 제네릭
Depth
에는 -1 부터 20 까지 하드코딩된 정수 배열에서Depth
인덱스에 해당하는 값을 새로 넣어준다.- 이 과정에서 재귀를 거치면서 자연스럽게
Depth
값이 1씩 줄어든다. Depth
값이 -1 이면done
을 반환하면서 재귀는 종료된다.
- 이 과정에서 재귀를 거치면서 자연스럽게
- 제네릭
- 조건문이 거짓이면 제네릭으로 받은
Arr
를 그대로 반환한다.
- 조건문이 참이면 재귀적으로
FlatArray
타입이 적용된 예시
result1
은 flat
함수의 매개 변수로 example
배열의 깊이인 9를 넣어준 후 얻은 결과이다. flat
의 반환 타입은 내부적으로 FlatArray
을 사용하기 때문에 number[]
으로 추론한다.
result2
는 하드코딩한 결과에 직접 FlatArray
타입을 선언해주었는데 타입 오류 없이 number[]
타입으로 추론되었다.
어렵다. 😬
내 설명이 깔끔하지 않아서 어려울 수도 있겠다는 생각이 든다. 어찌됐든 여기서 알아야 할 것은 extends
의 조건문과 infer
을 이용해서 재귀적인 추론을 하여 타입을 만들 수도 있었다는 사실이다.
마치며
infer
키워드를 사용한 다섯 가지 예제를 쉼 없이 살펴보았다. infer
키워드의 사용 방법에 대해 약간의 감이라도 잡았다면 나의 의도는 성공했다고 생각된다. 어떻게 글을 전개할까 고민을 했었는데, infer
키워드의 특징을 말로 서술하는 것보다 이렇게 예제를 여러 번 접해보는 것이 이해가 수월할 것이라 생각했다.
본문에서도 언급했지만 네 번째 예제에서 쓰인 문제는 TypeHero라는 웹 사이트에서 가져왔다. 주어진 문제에 대한 Type을 작성해 보는 문제풀이 웹 사이트로 난이도 별로 문제가 있으니 Easy 문제부터 쭉 풀어보는 걸 추천한다. Medium 난이도 정도까지 풀면 앞으로 타입 작성하는데 걱정이 크게 줄어들 것이다. 다른 사람의 풀이를 보는 것으로도 TypeScript에 대한 이해가 상당히 풍부해진다.
References