6 min read

Type‑Safe Currying & Partial Application in TypeScript: Patterns, Inference, and Real‑World Recipes

Learn how to write, type, and compose curried and partially‑applied functions in TypeScript with practical patterns that scale in production code.
Type‑Safe Currying & Partial Application in TypeScript: Patterns, Inference, and Real‑World Recipes

Introduction

Currying and partial application are classic functional‑programming techniques that turn a multi‑argument function into a chain of single‑argument functions (currying) or let you pre‑fill some arguments while leaving the rest for later (partial application). In JavaScript they are easy to write, but getting type safety in TypeScript can be tricky: you want the compiler to infer the remaining parameter types, preserve overloads, and still keep the resulting function callable without excessive casting.

This article walks through:

  • The core type definitions that make currying/partial‑application type‑safe.
  • How conditional and variadic tuple types power inference.
  • Re‑usable utility types for common patterns.
  • Real‑world examples – API clients, Redux‑style reducers, and UI event handlers.

By the end you’ll have a small toolbox you can drop into any TypeScript codebase and feel confident that the compiler will catch missing or misplaced arguments.


1. Why a Typed Curry Matters

Consider a naïve curry implementation:

function curry(fn: Function) {
  return function curried(...args: any[]) {
    if (args.length >= fn.length) return fn(...args);
    return (...more: any[]) => curried(...args, ...more);
  };
}

It works at runtime, but the returned function is typed as any. You lose:

  • Argument order checking – you can pass a string where a number is expected.
  • Autocomplete for remaining parameters – IDEs can’t suggest the next argument.
  • Preservation of overloads – a function with multiple call signatures collapses into a single any signature.

A type‑safe curry must propagate the original parameter list and track how many arguments have been supplied. TypeScript 4.0+ introduced variadic tuple types (...T) and recursive conditional types, which give us exactly the tools we need.


2. Core Types for a Type‑Safe Curry

2.1. Extracting the Parameter Tuple

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

These are the built‑in utility types, but we’ll alias them for readability:

type FnArgs<F> = Parameters<F>;
type FnResult<F> = ReturnType<F>;

2.2. The Recursive Curry Type

The idea: each call consumes at least one argument and returns either the final result (when the argument list is exhausted) or another curried function expecting the rest.

type Curried<F> =
  FnArgs<F> extends [infer A, ...infer Rest]               // at least one arg
    ? Rest['length'] extends 0                            // last argument?
      ? (a: A) => FnResult<F>                             // return result
      : (a: A) => Curried<(...args: Rest) => FnResult<F>> // recurse
    : never;

But this only works for single‑argument steps. To support supplying multiple arguments at once (e.g., curried(1, 2)), we need a more flexible version that accepts any prefix of the original parameters.

2.3. Flexible Curry with Prefix Inference

type Curry<F> =
  <T extends any[]>(...args: T) =>
    T extends FnArgs<F>                                 // all args supplied?
      ? FnResult<F>
      : Curry<(...args: Drop<FnArgs<F>, T['length']>) => FnResult<F>>;

/* Helper: Drop N items from a tuple */
type Drop<T extends any[], N extends number, Acc extends any[] = []> =
  Acc['length'] extends N ? T
    : T extends [any, ...infer Rest] ? Drop<Rest, N, [any, ...Acc]>
    : never;

Curry<F> is a generic call signature that:

  1. Accepts any tuple T.
  2. If T matches the whole parameter list, it returns the final result.
  3. Otherwise it returns a new curried function that expects the remaining parameters (computed with Drop).

Now the compiler can infer the exact shape of the remaining arguments after each partial call.


3. Implementing the Runtime Curry

The type definitions above live only at compile time. The runtime implementation mirrors the logic but stays simple:

function curry<F extends (...args: any[]) => any>(fn: F): Curry<F> {
  function curried(...args: any[]): any {
    if (args.length >= fn.length) {
      // @ts-ignore – we know the call is safe
      return fn(...args);
    }
    return (...more: any[]) => curried(...args, ...more);
  }
  return curried as any;
}

Because the heavy lifting is done by the type system, the runtime can stay loosely typed (any). The as any cast is safe: the compile‑time Curry<F> guarantees correct usage.


4. Partial Application with Preserved Types

Partial application is a special case of currying where you deliberately stop after a certain number of arguments. A tiny helper makes this explicit:

function partial<F extends (...args: any[]) => any, P extends any[]>(
  fn: F,
  ...preset: P
): (...rest: Drop<FnArgs<F>, P['length']>) => FnResult<F> {
  return (...rest) => fn(...preset, ...rest);
}

Usage:

const add = (a: number, b: number, c: number) => a + b + c;

const add5 = partial(add, 5);          // (b: number, c: number) => number
const result = add5(3, 2);             // 10

The generic Drop utility trims the original argument list, so the returned function’s signature exactly matches the remaining parameters.


5. Real‑World Patterns

5.1. Curried API Client

Imagine a small fetch wrapper that always needs a base URL and optional auth token:

type Request = {
  method: 'GET' | 'POST';
  path: string;
  body?: unknown;
};

type HttpClient = (req: Request) => Promise<any>;

const httpClient: HttpClient = async ({ method, path, body }) => {
  // simplified fetch
  const res = await fetch(path, { method, body: JSON.stringify(body) });
  return res.json();
};

We can curry it to pre‑bind the base URL:

type WithBase = (base: string) => Curried<(base: string, req: Request) => Promise<any>>;

const withBase: WithBase = (base) => curry(
  async (b: string, req: Request) => {
    const url = `${b}${req.path}`;
    return httpClient({ ...req, path: url });
  }
);

Now the consuming code gets excellent type support:

const api = withBase('https://api.example.com');

const getUser = api({ method: 'GET', path: '/users' }); // returns Promise<any>

If we later want to inject an auth token:

const withAuth = (token: string) => partial(api, token);
const authorized = withAuth('Bearer xyz');

authorized({ method: 'GET', path: '/profile' }).then(console.log);

Every step preserves the exact request shape, and IDEs autocomplete the remaining fields.

5.2. Redux‑Style Reducer Composition

A reducer has the signature (state: S, action: A) => S. Currying lets us create higher‑order reducers that pre‑apply a slice of the state:

type Reducer<S, A> = (state: S, action: A) => S;

function sliceReducer<S, A, K extends keyof S>(
  key: K,
  reducer: Reducer<S[K], A>
): Curried<Reducer<S, A>> {
  return curry((state: S, action: A) => ({
    ...state,
    [key]: reducer(state[key], action),
  }));
}

Usage:

interface Root {
  counter: number;
  todos: string[];
}

const inc = (n: number, _: any) => n + 1;

const counterReducer = sliceReducer('counter', inc);
const rootReducer = counterReducer; // curried

const next = rootReducer({ counter: 0, todos: [] }, {}); // {counter:1, todos:[]}

Because sliceReducer returns a Curried<Reducer<S, A>>, you can partially apply the state or the action depending on the call site, which is handy for testing isolated slices.

5.3. UI Event Handlers with Pre‑Bound Data

In React components you often need a click handler that receives an id from the component’s props and the synthetic event from the browser:

type ClickHandler = (id: string, e: React.MouseEvent) => void;

function onClick(id: string): Curried<ClickHandler> {
  return curry((i, e) => {
    console.log('Clicked', i, e.currentTarget);
  });
}

In JSX:

<button onClick={onClick(item.id)}>Delete</button>

TypeScript infers that onClick(item.id) returns a function expecting exactly a React.MouseEvent, so no any casts are needed.


6. Common Pitfalls & Tips

Pitfall Why it Happens Fix
Too many generic constraints Over‑constraining Curry<F> can cause the compiler to hit recursion limits. Keep the generic shallow; use helper types (Drop, Take) that are well‑tested.
Loss of overloads If F has overloads, Parameters<F> picks the last overload. Write a wrapper overload set manually or use a union of call signatures before currying.
Performance of type computation Complex tuple manipulations can slow down editor responsiveness in large files. Export the utility types from a dedicated types.ts file and import them where needed.
Runtime mismatch The runtime curry uses fn.length (the number of declared parameters). Default parameters or rest parameters break the count. Prefer explicit arity: curry(fn as (...args: any[]) => any, fn.length) or avoid default/rest in curried functions.

7. Packaging the Utilities

A minimal reusable module (curry.ts) could look like:

// ----- curry.ts -----
export type Drop<T extends any[], N extends number, Acc extends any[] = []> =
  Acc['length'] extends N ? T
    : T extends [any, ...infer Rest] ? Drop<Rest, N, [any, ...Acc]>
    : never;

export type Curry<F> =
  <T extends any[]>(...args: T) =>
    T extends Parameters<F>
      ? ReturnType<F>
      : Curry<(...args: Drop<Parameters<F>, T['length']>) => ReturnType<F>>;

export function curry<F extends (...args: any[]) => any>(fn: F): Curry<F> {
  function curried(...args: any[]): any {
    return args.length >= fn.length ? fn(...args) : (...more: any[]) => curried(...args, ...more);
  }
  return curried as any;
}

export function partial<F extends (...args: any[]) => any, P extends any[]>(
  fn: F,
  ...preset: P
): (...rest: Drop<Parameters<F>, P['length']>) => ReturnType<F> {
  return (...rest) => fn(...preset, ...rest);
}

Exporting these three symbols gives you a type‑safe currying toolkit you can import anywhere:

import { curry, partial, Curry } from './curry';

8. Conclusion

Currying and partial application are more than academic exercises; they enable function composition, dependency injection, and clean APIs in real‑world TypeScript projects. By leveraging variadic tuple types, conditional types, and a tiny runtime wrapper, you can achieve full type safety without sacrificing ergonomics.

Take the patterns presented here, drop the curry.ts module into your codebase, and start refactoring repetitive argument passing into composable, testable units. Your IDE will thank you, and your teammates will appreciate the extra compile‑time guarantees.

Happy functional programming!