ReturnType과 Parameters로 함수 타입 분해하기

TypeScript의 ReturnType<F>와 Parameters<F> 유틸리티 타입이 infer 키워드로 함수 타입을 분해하는 원리를 설명합니다. API 반환 타입 재사용, 래퍼 함수 시그니처 보존, memoize·throttle 고차 함수 패턴을 정리합니다.

· 5 min read · PALDYN Team

지난 글에서 유니언을 집합 연산으로 필터링하는 방법을 살펴봤습니다. 이번에는 함수 타입을 분해하는 ReturnType<F>Parameters<F>를 다룹니다. 이 두 유틸리티는 infer 키워드로 함수 타입의 안쪽을 들여다보며, 고차 함수 패턴에서 특히 빛을 발합니다.

ReturnType · Parameters 함수 분해

내부 구현: infer 키워드

// 반환 타입 추출
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

// 파라미터 타입을 튜플로 추출
type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

infer R은 조건부 타입이 참인 분기에서 TypeScript가 타입을 추론(infer)해서 R이라는 이름으로 바인딩하도록 합니다. 반환 타입을 직접 쓰지 않아도 함수 타입을 넘겨주는 것만으로 TypeScript가 알아냅니다.

ReturnType<F>: 반환 타입 추출

function createUser(name: string, role: "admin" | "user") {
  return { id: Math.random(), name, role, createdAt: new Date() };
}

// 반환 타입을 수동으로 정의하지 않아도 됨
type NewUser = ReturnType<typeof createUser>;
// → { id: number; name: string; role: "admin" | "user"; createdAt: Date }

typeof createUser는 함수 값의 타입을 가져오는 타입 레벨 연산자입니다. ReturnType<typeof fn>은 “이 함수가 반환하는 타입이 무엇인가?”를 표현하는 관용구입니다.

API 함수와 React Query

// API 함수 정의
async function fetchProduct(id: number) {
  const res = await fetch(`/api/products/${id}`);
  return res.json() as Promise<{ id: number; name: string; price: number }>;
}

// 반환 타입(Promise를 풀어서) 재사용
type Product = Awaited<ReturnType<typeof fetchProduct>>;

// React Query와 연동
function useProduct(id: number) {
  return useQuery<Product>({
    queryKey: ["product", id],
    queryFn: () => fetchProduct(id),
  });
}

함수 반환 타입이 바뀌면 Product 타입도 자동으로 갱신됩니다. 타입 정의가 단일 출처(함수 자체)에 집중됩니다.

Redux 셀렉터

const rootReducer = combineReducers({ auth: authReducer, cart: cartReducer });

// RootState를 별도로 정의하지 않아도
type RootState = ReturnType<typeof rootReducer>;

// 셀렉터 반환 타입 추출
const selectCartItems = (state: RootState) => state.cart.items;
type CartItems = ReturnType<typeof selectCartItems>;

Parameters<F>: 파라미터 튜플 추출

function sendEmail(to: string, subject: string, body: string): void {}

type EmailArgs = Parameters<typeof sendEmail>;
// → [to: string, subject: string, body: string]

// 튜플이므로 인덱스로 접근 가능
type RecipientType = EmailArgs[0]; // string

결과가 레이블드 튜플(labeled tuple) 이기 때문에 각 요소에 이름도 보존됩니다.

래퍼 함수 타입 보존

// Parameters와 ReturnType을 함께 써서 완전한 래퍼 작성
function withErrorBoundary<F extends (...args: any[]) => any>(fn: F) {
  return function(...args: Parameters<F>): ReturnType<F> | null {
    try {
      return fn(...args);
    } catch {
      return null;
    }
  };
}

const safeParseInt = withErrorBoundary(parseInt);
// safeParseInt의 타입: (string: string, radix?: number) => number | null

실전 패턴

ReturnType · Parameters 실전 패턴

고차 함수: memoize

function memoize<F extends (...args: any[]) => any>(fn: F): (
  ...args: Parameters<F>
) => ReturnType<F> {
  const cache = new Map<string, ReturnType<F>>();
  return (...args: Parameters<F>): ReturnType<F> => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = (n: number, factor: number) => n * factor * Math.PI;
const memoized = memoize(expensiveCalc);
// memoized: (n: number, factor: number) => number — 원래 타입 보존

함수 호출 로깅

function withLog<F extends (...args: any[]) => any>(fn: F, name: string) {
  return (...args: Parameters<F>): ReturnType<F> => {
    console.log(`[${name}] called with`, args);
    const result = fn(...args);
    console.log(`[${name}] returned`, result);
    return result;
  };
}

ConstructorParameters와 InstanceType

Parameters의 생성자 버전인 ConstructorParametersInstanceType도 같은 패턴입니다.

class Database {
  constructor(url: string, poolSize: number) {}
  query(sql: string): Promise<unknown[]> { return Promise.resolve([]); }
}

type DbArgs = ConstructorParameters<typeof Database>;
// → [url: string, poolSize: number]

type DbInstance = InstanceType<typeof Database>;
// → Database

// 팩토리 함수에서 활용
function createDb(...args: ConstructorParameters<typeof Database>): Database {
  return new Database(...args);
}

주의: 오버로드된 함수

함수에 오버로드가 여러 개 있을 때 ReturnTypeParameters마지막 오버로드 시그니처를 사용합니다.

function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number): number | string {
  return typeof input === "string" ? parseInt(input) : String(input);
}

type P = Parameters<typeof parse>; // [input: string | number] — 구현 시그니처
type R = ReturnType<typeof parse>;  // string | number

지난 글: Extract와 Exclude로 유니언 조각내기

다음 글: Awaited 유틸리티 타입 완전 정복


읽어주셔서 감사합니다. 😊