타입스크립트 infer 완벽 이해하기

Hushed_Mind·2025년 9월 26일

TypeScript

목록 보기
10/10
post-thumbnail

0) 도입 — “왜 굳이 infer가 필요할까?”

일반적인 타입 설계는 보통 “타입을 미리 적어두는” 방식이야. 그런데 실무에선 이미 어딘가에 정의된 타입 구조에서 “일부분만 꺼내 쓰고” 싶을 때가 많다.

  • 함수 시그니처에서 반환 타입만 가져오기
  • 함수의 파라미터 리스트(튜플)만 뽑기
  • Promise<...> 안의 결과 타입만 꺼내기
  • 문자열/튜플/객체 타입을 패턴 매칭해서 특정 조각만 얻기

이걸 매번 수동으로 재정의하면 중복불일치 지옥이 온다. 함수가 바뀌면 파생 타입도 전부 고쳐야 하거든.
infer는 이런 “기존 타입에서 특정 부분만 자동으로 추출”하는 도구다. 조건부 타입과 함께 쓰여서 패턴 매칭 + 타입 변수 추론을 가능하게 한다.

1) 기본 문법 — “조건부 타입 안에서만 쓴다”

형식은 딱 하나.

T extends SomePattern<infer U> ? U : Fallback
  • TSomePattern<...> 모양과 매치되면, 그 안의 일부를 infer U로 추론해서 U를 반환.
  • 매치 안 되면 Fallback (대개 never).

중요한 규칙

  • infer는 조건부 타입의 참( ? ) 분기 내부에서만 선언할 수 있다.
  • 한 번 추론된 U는 그 분기 안에서 이름을 가진 타입 변수처럼 자유롭게 쓸 수 있다.

2) 예제 1 — “함수 반환 타입 꺼내기” (ReturnType 원리)

type MyReturn<T> = T extends (...args: any[]) => infer R ? R : never;

function fn(x: number): string {
  return "hello";
}

type R = MyReturn<typeof fn>; // string

아주 자세한 뜯어보기

  1. typeof fn(x: number) => string 라는 값 → 타입 승격.

  2. 조건부 타입에 대입:

// T 자리에 (x: number) => string 이 들어감
( (x: number) => string ) extends (...args: any[]) => infer R ? R : never
  1. 좌변 함수 타입이 우변 패턴 (...args: any[]) => something 형태가 같다 → 매칭 성공.
    4.그 “something” 자리에 있는 타입을 infer R로 빼온다 → R = string.
    5.참 분기 결과 R → 최종 타입 string.

Tip
infer“형태가 맞으면 빼오는” 거다. 그래서 패턴 설계가 핵심이다.

3) 예제 2 — “파라미터 리스트(튜플) 꺼내기” (Parameters 원리)

type MyParams<T> = T extends (...args: infer P) => any ? P : never;

type P0 = MyParams<(a: number, b: string) => void>; // [number, string]

뜯어보기

  • 이번엔 (...args: infer P)에 함수의 파라미터 리스트 전체가 들어간다.
  • TS에서 파라미터 리스트는 튜플 타입으로 표현된다 → P = [number, string].
  • 그래서 결과는 [number, string].

주의
“튜플”이라서 순서/개별 타입이 고정된다. 단순한 any[]가 아니다.

4) 예제 3 — “Promise 내부 타입 풀기” (Awaited 원리, 재귀 포함)

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

뜯어보기

  1. Promise<string>이면 infer U = string → 다시 MyAwaited<string> 호출 → 더 이상 Promise 아님 → string.

  2. Promise<Promise<number>>

    • 1차: U = Promise<number>
    • 2차: U = number
    • 3차: number 반환.
  3. numberPromise<...> 패턴 불일치 → 그냥 T 그대로 반환.

Tip
재귀를 이용해 “여러 겹 Promise”도 풀어낼 수 있다.

5) 예제 4 — “튜플 구조 분해: 첫 원소 / 마지막 원소”

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아주 유사한 사고방식으로 보면 쉽다.

6) 예제 5 — “문자열 패턴 매칭” (템플릿 리터럴 + infer)

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"] 같은 것도 가능하다.

7) 분배 조건부 타입과 infer — “유니온을 어떻게 처리하나”

조건부 타입은 좌변이 유니온이면 분배(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; // 분배 억제 트릭

8) infer의 제약/한계 — “여기선 안 된다”

  1. 조건부 타입의 참 분기 내부에서만 선언 가능.
    아래처럼 거짓 분기에서 선언은 문법적으로 불가.
// ❌ 잘못된 예
type Bad<T> = T extends any ? string : infer X;
  1. 모호하면 추론 실패never로 빠지거나 원하는 결과가 안 나온다.
  2. 여러 곳에서 상충되는 추론이 생기면 실패할 수 있다. (서로 다른 타입을 한 변수에 동시에 강제하면 안 됨)
  3. 함수 파라미터(반공변 위치) 추론 난이도
    함수 타입에서 입력 위치는 반공변이라 추론이 까다롭다. 가능한 한 출력 위치(공변)에서 추론하는 패턴이 안정적.

9) 자주 쓰는 패턴 정리

9-1. ReturnType / Parameters

type MyReturn<T>  = T extends (...a: any[]) => infer R ? R : never;
type MyParams<T>  = T extends (...a: infer P) => any ? P : never;

9-2. Awaited (중첩 Promise 풀기)

type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;

9-3. 튜플 분해

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;

9-4. 템플릿 리터럴 파싱

type TrimLeft<S extends string> =
  S extends ` ${infer R}` | `\n${infer R}` | `\t${infer R}` ? TrimLeft<R> : S;

10) 실전 미니 응용 — “API 응답 타입 자동 추출 + 느슨화”

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 Rresponse만 추출
  • Loose<T>로 리터럴/readonly를 실사용-friendly 타입으로 변환

11) 디버깅 요령 — “왜 내가 원하는 대로 추론이 안 되지?”

  • 분배가 개입했는지 확인: 좌변이 유니온이면 각 원소에 따로 조건이 적용된다.
    원치 않으면 [T] extends [...] 트릭으로 분배를 끊어라.
  • 패턴이 너무 빡빡한지 확인: 매치 실패하면 전부 never로 빠진다. 패턴을 느슨하게 바꿔보자.
  • 반공변 위치(함수 파라미터)에서 추론하려고 집착하지 말고, 공변 위치(반환값/프로퍼티)로 옮겨서 추론을 유도하라.
  • 추론된 타입 확인: IDE에서 마우스 오버하거나, 보조 타입으로 노출해서 확인해라.
type Debug<T> = T extends any ? { result: T } : never;
type D = Debug<MyReturn<typeof fn>>;

12) 최종 정리

  • infer조건부 타입의 참 분기에서 패턴 매칭으로 타입을 꺼내는 도구다.
  • 핵심은 “패턴을 어떻게 설계하느냐”: 함수/튜플/템플릿 리터럴/객체 어디든 응용 가능.
  • 분배 조건부 타입을 이해해야 예측 가능한 결과를 얻는다.

관광데이터 공모전 준비를 하느라 2달동안 블로그를 못썼다..ㅠㅠ 이제 제출을 해서 다시 타입스크립트 공부를 시작했다! 다시 일주일에 하나씩 써보도록 해야겠다!!

profile
개발 공부 블로그

0개의 댓글