
일반적인 타입 설계는 보통 “타입을 미리 적어두는” 방식이야. 그런데 실무에선 이미 어딘가에 정의된 타입 구조에서 “일부분만 꺼내 쓰고” 싶을 때가 많다.
이걸 매번 수동으로 재정의하면 중복과 불일치 지옥이 온다. 함수가 바뀌면 파생 타입도 전부 고쳐야 하거든.
infer는 이런 “기존 타입에서 특정 부분만 자동으로 추출”하는 도구다. 조건부 타입과 함께 쓰여서 패턴 매칭 + 타입 변수 추론을 가능하게 한다.
형식은 딱 하나.
T extends SomePattern<infer U> ? U : Fallback
T가 SomePattern<...> 모양과 매치되면, 그 안의 일부를 infer U로 추론해서 U를 반환.Fallback (대개 never).중요한 규칙
- infer는 조건부 타입의 참( ? ) 분기 내부에서만 선언할 수 있다.
- 한 번 추론된
U는 그 분기 안에서 이름을 가진 타입 변수처럼 자유롭게 쓸 수 있다.
type MyReturn<T> = T extends (...args: any[]) => infer R ? R : never;
function fn(x: number): string {
return "hello";
}
type R = MyReturn<typeof fn>; // string
typeof fn → (x: number) => string 라는 값 → 타입 승격.
조건부 타입에 대입:
// T 자리에 (x: number) => string 이 들어감
( (x: number) => string ) extends (...args: any[]) => infer R ? R : never
(...args: any[]) => something 과 형태가 같다 → 매칭 성공.infer R로 빼온다 → R = string.R → 최종 타입 string.Tip
infer는 “형태가 맞으면 빼오는” 거다. 그래서 패턴 설계가 핵심이다.
type MyParams<T> = T extends (...args: infer P) => any ? P : never;
type P0 = MyParams<(a: number, b: string) => void>; // [number, string]
(...args: infer P)에 함수의 파라미터 리스트 전체가 들어간다.P = [number, string].[number, string].주의
“튜플”이라서 순서/개별 타입이 고정된다. 단순한 any[]가 아니다.
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
type A = MyAwaited<Promise<string>>; // string
type B = MyAwaited<Promise<Promise<number>>>; // number
type C = MyAwaited<number>; // number
Promise<string>이면 infer U = string → 다시 MyAwaited<string> 호출 → 더 이상 Promise 아님 → string.
Promise<Promise<number>>면
U = Promise<number>U = numbernumber 반환.number는 Promise<...> 패턴 불일치 → 그냥 T 그대로 반환.
Tip
재귀를 이용해 “여러 겹 Promise”도 풀어낼 수 있다.
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type F1 = First<["a", "b", "c"]>; // "a"
type L1 = Last<["a", "b", "c"]>; // "c"
type F2 = First<[]>; // never (매칭 불가)
[infer F, ...any[]]는 “첫 칸을 F로 받고 나머지는 아무거나”라는 패턴.["a","b","c"]는 위 패턴과 매치 → F = "a".never.이해 포인트
값에서 구조 분해const [first, ...rest] = arr과 아주 유사한 사고방식으로 보면 쉽다.
type Split2<S extends string> =
S extends `${infer A}-${infer B}` ? [A, B] : [S];
type S1 = Split2<"ab-cd">; // ["ab","cd"]
type S2 = Split2<"hello">; // ["hello"]
infer를 쓸 수 있다."ab-cd"는 ${A}-${B} 패턴과 매칭 → A = "ab", B = "cd"."hello"는 -가 없어서 매칭 실패 → 기본값 [S].확장 아이디어
재귀를 쓰면Split<"a-b-c","-"> → ["a","b","c"]같은 것도 가능하다.
조건부 타입은 좌변이 유니온이면 분배(distributive) 된다. 즉 각 원소에 독립적으로 조건을 적용하고 합친다.
type Element<T> = T extends (infer U)[] ? U : T;
type E1 = Element<string[]>; // string
type E2 = Element<(number | boolean)[]>; // number | boolean
type E3 = Element<string | number[]>; // string | number
// 해석: Element<string> | Element<number[]>
// -> string | number
[T = string | number[]Element<string> | Element<number[]>string | number.주의 포인트
분배가 원치 않게 일어날 때가 있다. 그땐T를 한 번 감싸 분배를 끊는다.type NoDistrib<T> = [T] extends [infer U] ? U : never; // 분배 억제 트릭
// ❌ 잘못된 예
type Bad<T> = T extends any ? string : infer X;
never로 빠지거나 원하는 결과가 안 나온다.type MyReturn<T> = T extends (...a: any[]) => infer R ? R : never;
type MyParams<T> = T extends (...a: infer P) => any ? P : never;
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : [];
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type TrimLeft<S extends string> =
S extends ` ${infer R}` | `\n${infer R}` | `\t${infer R}` ? TrimLeft<R> : S;
const endpoints = {
getUser: { url: "/user", response: { id: 1, name: "string" } },
getPosts: { url: "/posts", response: [{ id: 1, title: "string" }] },
} as const;
type ApiResponse<T> = T extends { response: infer R } ? R : never;
function fetchApi<K extends keyof typeof endpoints>(
key: K
): ApiResponse<(typeof endpoints)[K]> {
return null as any;
}
const user = fetchApi("getUser");
// 타입: { readonly id: 1; readonly name: "string" }
const posts = fetchApi("getPosts");
// 타입: readonly [{ readonly id: 1; readonly title: "string" }]
// 리터럴/readonly를 일반 타입으로 느슨화
type Loose<T> =
T extends string | number | boolean ? (T extends string ? string : T extends number ? number : boolean)
: T extends readonly (infer U)[] ? Loose<U>[]
: T extends object ? { [K in keyof T]: Loose<T[K]> }
: T;
type User = Loose<typeof user>; // { id: number; name: string }
type Posts = Loose<typeof posts>; // { id: number; title: string }[]
typeof endpoints로 값→타입을 뽑고keyof로 가능한 키를 유니온으로 제한(typeof endpoints)[K]로 특정 엔드포인트 타입 선택ApiResponse<...>에서 infer R로 response만 추출Loose<T>로 리터럴/readonly를 실사용-friendly 타입으로 변환[T] extends [...] 트릭으로 분배를 끊어라.never로 빠진다. 패턴을 느슨하게 바꿔보자.type Debug<T> = T extends any ? { result: T } : never;
type D = Debug<MyReturn<typeof fn>>;
infer는 조건부 타입의 참 분기에서 패턴 매칭으로 타입을 꺼내는 도구다.관광데이터 공모전 준비를 하느라 2달동안 블로그를 못썼다..ㅠㅠ 이제 제출을 해서 다시 타입스크립트 공부를 시작했다! 다시 일주일에 하나씩 써보도록 해야겠다!!