자기 자신을 타입으로 사용하는 재귀 타입이 있다
재귀 함수와 재귀 타입
자바스크립트에서 재귀 함수란 자기 자신을 호출하는 함수이다. 예를 들어, 피보나치 수열을 계산하는 다음의 함수는 자기 자신을 반복 호출하여 값을 계산한다.
function fibonacci(num) {
if (num <= 1) return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
}
이와 같이 함수 내부에서 자기 자신을 호출하는 방식이 재귀 함수이며, 종료 조건이 없으면 Maximum call stack size exceeded와 같은 에러가 발생한다.
타입스크립트에서도 재귀적인 개념을 타입에 적용할 수 있는데, 이를 재귀 타입이라고 한다. 재귀 타입은 타입의 정의 안에서 자기 자신을 다시 참조하는 형태이다. 예를 들어, 다음 코드는 객체 타입인 Recursive를 정의할 때, children 속성의 타입으로 다시 Recursive를 사용하는 재귀 타입의 예이다.
type Recursive = {
name: string;
children: Recursive[];
};
const recurl: Recursive = {
name: 'test',
children: [],
};
const recur: Recursive = {
name: 'test',
children: [
{ name: 'test2', children: [] },
{ name: 'test3', children: [] }
]
};
이처럼 객체의 구조가 재귀적으로 정의되어 있는 경우에 재귀 타입을 사용하여 타입을 표현할 수 있다.
조건부 타입과 재귀 타입
재귀 타입은 조건부 타입과 함께 사용할 수 있다. 다음 코드는 배열의 요소 타입을 추출하기 위해 재귀적으로 타입을 해체하는 예이다.
type ElementType<T> = T extends any[] ? ElementType<T[number]> : T;
하지만 타입 인수를 직접 재귀 타입에 사용하는 경우에는 에러가 발생할 수 있다. 예를 들어, 다음과 같이 타입 인수를 사용하면 순환 참조 문제가 발생하여 Type alias 'T' circularly references itself. 에러가 발생한다.
// 에러 발생: 타입 인수를 사용한 경우
type T = number | string | Record<string, T>;
따라서 타입 인수를 사용하지 않는 방식으로 수정하면 문제가 해결된다.
type T = number | string | { [key: string]: T };
재귀 타입 사용 시 주의점
자바스크립트의 재귀 함수가 종료 조건이 없으면 스택 오버플로우 에러가 발생하는 것처럼, 타입스크립트에서도 재귀 타입을 사용할 때는 무한 순환에 주의해야 한다. 예를 들어, 무한히 중첩된 타입을 생성하는 경우 아래와 같이 에러가 발생한다.
type InfiniteRecur<T> = { item: InfiniteRecur<T> };
type Unwrap<T> = T extends { item: infer U } ? Unwrap<U> : T;
type Result = Unwrap<InfiniteRecur<any>>; // "Type instantiation is excessively deep and possibly infinite." 에러 발생
위의 코드에서 InfiniteRecur 타입은 무한히 중첩된 item 속성을 가지므로, 이를 Unwrap 타입으로 처리하려 할 때 타입스크립트가 처리할 수 없음을 경고한다.
재귀 타입의 활용 예시: JSON 타입 정의
JSON은 문자열, 숫자, 불 값, null 또는 다른 JSON 값들로 구성된다. JSON 구조는 중첩될 수 있으므로 재귀 타입으로 표현하는 것이 적합하다. 다음은 JSON 타입을 재귀 타입을 이용하여 정의한 예이다.
type JSONType =
| string
| number
| boolean
| null
| JSONType[]
| { [key: string]: JSONType };
const a: JSONType = "string";
const b: JSONType = [1, false, { hi: "json" }];
const c: JSONType = {
prop: null,
arr: [{}],
};
이처럼 재귀 타입을 사용하면 복잡한 데이터 구조를 간결하게 표현할 수 있다.
재귀 타입을 이용한 배열 역순 및 함수 매개변수 순서 뒤집기
재귀 타입는 배열의 타입을 역순으로 만드는 등 다양한 활용이 가능하다. 예를 들어, 배열 [1, 2, 3]의 타입을 [3, 2, 1]로 바꾸기 위한 코드는 다음과 같다.
type Reverse<T> = T extends [...infer L, infer R] ? [R, ...Reverse<L>] : [];
동일한 원리를 응용하여 함수의 매개변수 순서를 뒤집는 타입을 만들 수도 있다. 아래 코드는 함수의 매개변수 타입을 추론한 후, Reverse 타입을 적용하여 순서를 반전시킨다.
type FlipArguments<T> = T extends (...args: infer A) => infer R
? (...args: Reverse<A>) => R
: never;
type Flipped = FlipArguments<(a: string, b: number, c: boolean) => string>;
// Flipped 타입은 (args_0: boolean, args_1: number, args_2: string) => string 이 된다.
재귀 타입은 위와 같이 간결한 코드로 복잡한 타입 변환을 가능하게 하며, 프로그래밍의 생산성을 높이는 데 기여한다.