이번 학습을 통하여
이 문서는 TypeScript의 조건부 타입과 infer를 처음 접하는 분도 쉽게 이해할 수 있도록 다시 정리한 학습용 버전입니다. 복잡한 타입을 자동으로 추출하거나 배열과 함수·API 타입을 분석하는 방법을 단계별로 배울 수 있게 구성했습니다.
- 조건부 타입과 infer 개념 정리
- infer 사용 전/후 비교
- 실전 예제 1: 배열 내부 타입 평탄화
- 실전 예제 2: 함수 반환 타입 추출
- 실전 예제 3: API 응답 타입 자동화
- 타입 변화 디버깅(조건 성립 여부)
- 결론 및 활용 전략
- FAQ
조건부 타입과 infer 개념 정리
조건부 타입은 T extends U ? X : Y 형태로, T가 U를 만족하는지 여부에 따라 반환 타입이 달라지는 구조입니다.
여기에 infer 키워드를 사용하면 조건부 타입 내부에서 새로운 타입 변수를 선언하여 특정 부분의 타입을 추출할 수 있습니다.
infer 작동 구조 시각화
T extends (...args: any[]) => infer R ? R : never
1) 먼저 T가 extends로 "(...args: any[]) => infer R" 함수처럼 생겼는지 확인합니다.
- 형태만 보면 됩니다: (무언가를 받고 → 무언가를 돌려주는 모양)
- 즉, T가 함수 모양이면 조건을 만족합니다.
2) 함수라고 확인되면, 해당 함수가 "어떤 값을 반환하는지" 확인하고 그 반환 타입을 그대로 꺼내옵니다.
- 함수는 (매개변수) ⇒ 반환값(return 타입) 구조입니다.
- infer R 은 이 중에서 "반환값(return 타입) 자리"에 있는 타입을 R 이름으로 붙여 가져옵니다.
3) 최종 결과 타입은 방금 꺼낸 R 이 됩니다.
- 즉, infer는 "함수의 반환 타입만 골라내는 도구"처럼 동작합니다.
infer를 사용하지 않은 조건부 타입 예제
type Test<T> = T extends string ? true : false;
type X = Test<"hi">; // true
type Y = Test<number>; // false (infer 없음)
infer를 사용하지 않으면 함수·배열·API 구조에서 필요한 타입만 추출하기가 매우 어려워집니다. 예를 들어 함수의 반환 타입을 수동으로 적어 넣어야 하거나, 중첩 배열 내부 타입을 직접 계산해야 하는 불편함이 생깁니다. 그래서 조건부 타입 안에서 특정 위치의 타입을 자동으로 꺼내주는 infer가 실전에서 큰 장점이 됩니다.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
위 타입은 T가 함수 타입일 때 그 반환 타입 R을 추론하여 반환합니다. 함수가 아니라면 never가 반환됩니다.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
위 타입은 T가 함수 타입일 때 그 반환 타입 R을 추론하여 반환합니다. 함수가 아니라면 never가 반환됩니다.
infer 사용 전/후 비교
Before: infer 없이 반환 타입 추출이 어려움
function foo() {
return { msg: "hello" };
}
type Result = /* 반환 타입 직접 추출 불가 */
After: infer로 함수 반환 타입을 추론
type GetReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type FooReturn = GetReturn<typeof foo>; // { msg: string }
실전 예제 1: 배열 내부 타입 평탄화
중첩 배열에서 가장 깊은 원소의 타입을 추출하려면 재귀 조건부 타입과 infer의 조합이 필요합니다.
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;
위 타입은 배열 T의 내부 타입을 재귀적으로 추론하여 최종 원소의 타입을 반환합니다.
타입 변화 디버깅 과정
Flatten이 내부 타입을 어떻게 단계별로 좁혀 나가는지 살펴보면 동작 방식이 훨씬 쉽게 이해됩니다. 아래 표는 Flatten<number[][][]>가 최종적으로 number에 도달하기까지의 흐름을 보여줍니다.
| 단계 | T 값 | 조건 성립 여부 | 추론 결과 |
|---|---|---|---|
| 1 | number[][][] | 배열 O | U = number[][] |
| 2 | number[][] | 배열 O | U = number[] |
| 3 | number[] | 배열 O | U = number |
| 4 | number | 배열 X | 최종 반환 타입 = number |
실전 배열 변환 예시
type A = Flatten<number[][][]>; // number
type B = Flatten<string[]>; // string
type C = Flatten<boolean>; // boolean (배열 아님 → 즉시 반환)
추가 실전 배열 변환 예시
// 배열 내부 객체 타입 추출
const data = [[[{ id: 1, name: "Kim" }]]];
type Extracted = Flatten;
// { id: number; name: string }
// 혼합 타입 배열 평탄화
type Mixed = Flatten<([number[]] | string[][])[]>; // number | string
실전 예제 2: 함수 반환 타입 추출
infer는 복잡한 함수의 반환 타입을 자동으로 추론하는 데 특히 유용합니다.
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
함수 반환 타입 추론 디버깅 단계
| 단계 | T 값 | 조건(T extends (…args) ⇒ ?) | 추론 결과 |
|---|---|---|---|
| 1 | typeof fetchUser | 함수 O | R = { id: number; name: string } |
| 2 | typeof getPromise | 함수 O | R = Promise<number> |
| 3 | 일반 타입(예: number) | 함수 X | never |
실전 함수형 변환 예시
function fetchUser() {
return { id: 1, name: "Alice" };
}
type User = GetReturnType<typeof fetchUser>; // { id: number; name: string }
function getPromise() {
return Promise.resolve(123);
}
type Result = GetReturnType<typeof getPromise>; // Promise<number>
추가 실전 함수형 변환 예시
// 함수 조합(composition)에서의 반환 타입 분석
function toUpper(value: string) {
return value.toUpperCase();
}
type UpperReturn = GetReturnType; // string
function wrap(value: string) {
return { text: value, length: value.length };
}
type WrapReturn = GetReturnType; // { text: string; length: number }
// 콜백 구조 분석
function process(cb: () => number[]) {
return cb().map(v => v * 2);
}
type ProcessReturn = GetReturnType; // number[]
실전 예제 3: API 응답 구조 분석
Zod와 같은 검증 라이브러리는 infer 기반 타입 추론과 결합하여 뛰어난 타입 안정성을 제공합니다.
import { z } from "zod";
const schema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type ApiResponse = z.infer; // { id: number; name: string; email: string }
async function fetchData() {
const res = await fetch("/api/user");
const json = await res.json();
return schema.parse(json);
}
type FetchDataReturn = ReturnType
// fetchData() 호출 시 실제 반환 타입 예시
// fetchData(): Promise<{ id: number; name: string; email: string }>
// 즉, API 응답 구조가 코드에서 자동으로 타입으로 보장됩니다.
결론 및 활용 전략
조건부 타입과 infer 키워드는 복잡한 타입 구조를 단순화하고, API·함수·배열 등 다양한 실전 시나리오에서 타입 안정성을 극대화합니다. 특히 대규모 코드베이스에서 타입 추론 자동화를 구현할 때 필수적인 기능입니다.
FAQ
Q1. infer는 왜 조건부 타입 안에서만 사용할 수 있나요?
infer는 “조건이 성립했을 때만 타입을 추론하는 구조”이기 때문에 조건부 타입 내부에서만 의미가 생깁니다. 조건이 false이면 infer는 평가되지 않아 타입 안정성을 보장합니다.
Q2. Flatten 타입은 재귀가 언제 멈추나요?
배열 여부(T extends (infer U)[])를 검사하여 배열이면 재귀 진행, 배열이 아니면 T 그대로 반환하며 종료됩니다.
Q3. 매우 깊은 중첩 배열(number[][][][]…)에서도 안전한가요?
일반적인 프로젝트에서는 문제 없습니다. 다만 수백 단계 재귀가 포함된 경우 타입 계산량 증가로 빌드 속도가 느려질 수 있습니다.
Q4. GetReturnType이 실제 함수 반환값과 다르게 추론될 수도 있나요?
가능합니다. 조건부 타입 기반 추론은 “겉으로 보이는 함수 시그니처”를 기준으로 하기 때문에 오버로드, 제네릭, 인터섹션 타입에서는 실제 동작과 다른 결과가 나올 수 있습니다.
Q5. API 검증 스키마에서 infer를 쓰면 무엇이 좋은가요?
스키마 중심 개발이 가능합니다. 스키마를 변경하면 타입이 자동 갱신되어 타입 관리 비용이 크게 줄어듭니다.
Q6. never가 자주 등장하는 이유는 무엇인가요?
조건이 불일치할 때 반환되는 기본 타입이 never이기 때문입니다. never는 “일어날 수 없는 값”을 의미하여 조건부 타입에서 매우 빈번하게 사용됩니다.
Q7. infer로 추론된 타입을 가공할 수 있나요?
가능합니다. 예를 들어 조합된 타입이나 래핑 타입을 만들 수 있습니다.type Wrapped<T> = T extends (...args: any[]) => infer R ? { result: R } : never;
Q8. infer는 실전에서 어떤 문제를 해결하나요?
실전 활용 예: API 응답 자동 타입 생성, 중첩 자료구조 평탄화, 함수 조합 시스템 분석, 콜백 기반 로직 추론 등 다양한 영역에서 타입 안정성과 개발 효율이 크게 증가합니다.
Q9. infer를 잘못 사용하면 어떤 문제가 발생하나요?
조건이 과하게 넓게 매칭되면 타입이 무너질 수 있고, 재귀 사용 시 복잡성이 증가하여 성능 저하를 일으킬 수 있습니다.
Q10. 조건부 타입과 infer는 반드시 함께 써야 하나요?
아닙니다. 조건부 타입만으로도 강력하지만, infer가 결합되면 타입 내부의 부분 추출까지 가능해져 표현력이 극대화됩니다.