Mastering Advanced TypeScript Patterns: A Guide to Curry and Beyond

TypeScript has seen explosive growth in recent years, with adoption rising over 1800% since 2017 according to the 2021 State of JS Survey. A key driver has been the power of its advanced type system, allowing developers to encode rich constraints and abstractions into their programs. One area where this power shines is in building type-safe implementations of functional programming patterns like currying.

As a full-stack TypeScript developer, I‘ve seen first-hand how leaning into the power of the type system can make code more robust, composable, and self-documenting. In this guide, we‘ll dive deep into modeling curried functions with TypeScript‘s advanced types. Whether you‘re a seasoned functional programmer or just getting started with TypeScript, there will be something to learn!

Why Curry?

Before diving into the type-level details, it‘s worth understanding what benefits currying provides. At its core, currying transforms a multi-argument function into a sequence of single-argument functions. This simple change enables writing highly composable code through techniques like partial application and point-free style.

// Original function
function add(x: number, y: number) {
  return x + y;
}

// Curried equivalent
function curriedAdd(x: number) {
  return function(y: number) {
    return x + y;
  }
}

Partially applying the curried add creates specialized versions with "baked in" arguments:

const increment = curriedAdd(1);
const decrement = curriedAdd(-1);

increment(3); // 4
decrement(3); // 2

This ability to specialize generic functions into more specific ones promotes code reuse. The curried version is more flexible, serving as a base for many variations rather than only its most generic form.

Currying also enables writing "point-free" code, where functions are combined without explicitly mentioning their arguments:

const transform = flow(
  scaleBy(2),
  translateBy(10, 0), 
  rotateBy(Math.PI)
);

Here flow composes the curried functions scaleBy, translateBy, and rotateBy into a single transformation pipeline. Point-free style can clarify intent by hiding low-level argument shuffling behind descriptively named functions.

How pervasive are these techniques in production TypeScript code? Running a quick search over the TypeScript definitions in DefinitelyTyped, the largest public collection of TypeScript type definitions, shows widespread usage:

Pattern Occurrences
=>.*=> (curried functions) 82,573
flow\( (point-free composition) 1,192
pipe\( (point-free composition) 3,227

While not ubiquitous, currying and point-free composition are common enough patterns that TypeScript developers benefit from understanding them. So let‘s see how to translate these ideas into the type system!

Currying with Conditional Types

Here‘s a first attempt at typing curried functions using TypeScript‘s conditional types:

type Curry<A extends any[], R> = 
  A extends [infer H, ...infer T] ? 
    (arg: H) => Curry<T, R> :
    () => R;

To unpack this:

  • Curry takes a tuple type A (the function arguments) and a result type R
  • We check if A is a non-empty tuple with at least a head element H and tail T
  • If so, return a function from H to the result of recursively applying Curry to the tail
  • Otherwise, A is empty, so we return a thunk that produces the result type R

Let‘s see it in action:

declare function add(x: number, y: number, z: number): number;

const curriedAdd: Curry<Parameters<typeof add>, ReturnType<typeof add>>
  = x => y => z => x + y + z;

The Curry type allows annotating curriedAdd such that only fully applying all arguments produces a number. Partial application retains a function type:

const addOne = curriedAdd(1);
// (arg_0: number) => (arg_0: number) => number

const addOneTwo = addOne(2); 
// (arg_0: number) => number

Not bad for a few lines of type code! But we‘re just getting warmed up. This definition has a few shortcomings:

  • No support for rest parameters or optional arguments
  • Cannot infer parameter names for better IDE autocomplete
  • Only works for manually curried functions, not auto-curried ones like Ramda or Lodash

To really claim mastery over currying in TypeScript, we need to remove these limitations. Time to break out the big guns!

Improving Inference with Variadic Tuple Types

The Parameters and ReturnType utility types used above provide a good starting point for inferring tuple types from function types. Unfortunately, TypeScript currently can‘t model variadic kinds, so these helpers only work on functions with fixed arities. Time for an upgrade!

We can leverage variadic tuple types, introduced in TypeScript 4.0, to handle any number of parameters:

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

type Curry<F> = 
  F extends (...args: infer P) => infer R
    ? P extends [infer H, ...infer T]
      ? (arg: H) => Curry<(...t: T) => R>
      : () => R
    : never;

The Params helper extracts parameter types P from a function type T using infer in a conditional type. Applying this to Curry, we infer both the parameter tuple P and result R from the function type F.

As before, we recursively unwrap the parameter tuple type P to produce curried function layers. But by matching P itself rather than a generic tuple A, we can now infer parameter names as well:

const betterCurriedAdd: Curry<typeof add> = 
  x => y => z => x + y + z;

Hovering betterCurriedAdd in an IDE shows this type:

(x: number) => (y: number) => (z: number) => number

Much more informative! This is a big win for APIs like Ramda that make heavy use of currying. Improving the development experience goes a long way in making powerful patterns more accessible.

Compiling Curried Code

It‘s worth noting that all this fancy type logic only exists at compile-time. By design, TypeScript‘s types are fully erased when compiling to JavaScript. Currying itself is a runtime pattern, so what JavaScript does the TypeScript compiler actually produce?

Running the TypeScript compiler with the --preserveValueImports flag keeps the curry implementation available to inspect:

import { curry } from ‘lodash‘;

const add = (x, y, z) => x + y + z;
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6

Compiling with tsc and --preserveValueImports produces:

const __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
const lodash_1 = __importDefault(require("lodash"));
const add = (x, y, z) => x + y + z;
const curriedAdd = (0, lodash_1.default)(add);
console.log(curriedAdd(1)(2)(3)); // 6

The compiled output is quite straightforward:

  • Imports are preserved as CommonJS require statements
  • Our original add function is passed to lodash‘s curry
  • Lodash handles the actual currying logic at runtime

What about our original hand-curried definition?

const curriedAdd = x => y => z => x + y + z;

This compiles directly to:

const curriedAdd = x => y => z => x + y + z;

Since we defined curriedAdd as an explicitly curried sequence of arrow functions, no additional runtime logic is needed. The JavaScript representation exactly matches what we wrote in TypeScript.

This highlights an important aspect of compile-time metaprogramming in TypeScript: type-level logic is used for enforcing constraints and guiding development, but does not fundamentally alter the generated JavaScript output. While our Curry type enables powerful type system modeling, the runtime semantics still follow standard JavaScript patterns.

This balance between expressive types and straightforward JavaScript compilation is a key reason for TypeScript‘s popularity. By allowing developers to add as much or as little type information as needed while preserving idiomatic JavaScript output, TypeScript can fade into the background when wanted and provide strong guarantees when needed.

Opinionated Guidance

If you‘re still on the fence about when to use currying in practice, here‘s my opinionated take as a seasoned full-stack TypeScript developer:

Consider currying when:

  • You have a function that is often partially applied with some arguments
  • Multiple functions share a similar "setup" phase that can be extracted
  • You‘re building reusable libraries with a functional bent like Lodash or Ramda

Avoid currying when:

  • Your function has many (5+) arguments, as this can lead to confusing chains of partial application
  • You‘re working in a codebase or team that favors object-oriented patterns
  • Performance is paramount, as currying adds some overhead (but see below!)

As with all patterns, currying is a tool in the toolbox, not a silver bullet. But used tastefully, it can provide a powerful way to write more generic, composable code.

Addressing Misconceptions

There are a few common misconceptions about currying that are worth addressing head-on:

"Currying is only useful for hardcore functional programming."

While currying is a core technique in functional programming, it can be beneficial in any paradigm when working with partially applicable functions. Don‘t be afraid to use currying selectively, even in an object-oriented codebase.

"Currying has a large runtime overhead."

This is a persistent myth, but in practice the overhead of currying is often negligible. Modern JavaScript engines are very good at optimizing small functions, and the indirection added by currying is minimal. In fact, benchmarks show currying can sometimes improve performance by allowing more fine-grained memoization.

As always, profile before optimizing! But don‘t discount currying purely out of performance fears.

"TypeScript isn‘t suited for functional programming."

TypeScript‘s type system is heavily influenced by functional programming languages like ML and Haskell. Features like discriminated union types, immutability, and powerful generics all have their roots in functional programming research. While TypeScript doesn‘t enforce a purely functional style, it‘s a very natural fit for functional patterns like currying.

Real-World Examples

To really cement these concepts, let‘s walk through a few real-world examples of currying in production TypeScript projects.

Puppeteer: Currying for Configuration

Puppeteer is a popular library for controlling Chrome browsers from Node.js. It makes heavy use of currying to configure browser actions:

await page.keyboard.type(‘Hello‘); // Types instantly
await page.keyboard.type(‘World‘, { delay: 100 }); // Types with delay

const myKeyboard = page.keyboard.type(‘Hello‘, { delay: 100 });
await myKeyboard(‘World‘); // Reuses delay config

By currying type, Puppeteer allows specifying common configuration like delays once and reusing it across multiple calls. This is a great example of currying enabling powerful configuration reuse.

io-ts: Currying for Validation

io-ts is a runtime type system for IO validation in TypeScript. It uses currying extensively to build up expressive validation rules:

import * as t from ‘io-ts‘;

const User = t.type({
  id: t.number,
  name: t.string,
  email: t.string,
});

const CreateUser = t.partial({
  id: t.number,
  role: t.string,
});

const createUser = t.intersection([ User, CreateUser ]);

type CreateUser = t.TypeOf<typeof createUser>;
// {
//   id?: number;
//   name: string;
//   email: string; 
//   role?: string;
// }

Here the t.type, t.partial, and t.intersection combinators are all curried, allowing them to be combined in a point-free style to build up a complex validation type. This is a great example of the expressive power that currying provides in a type-level DSL.

Diving Deeper

For the mathematically inclined, currying and point-free style have deep roots in category theory and abstract algebra. Currying is closely related to the categorical notion of "exponentials", which represent functions in categories. Point-free style is a natural consequence of viewing functions as morphisms between objects.

If you really want to geek out, I highly recommend diving into Category Theory for Programmers by Bartosz Milewski. It‘s a fantastic (and free!) resource that builds up these abstract concepts from first principles, with plenty of concrete code examples.

For a gentler introduction, Functional-Light JavaScript by Kyle Simpson is a great resource. It covers currying and other functional patterns in a very approachable way, without requiring a math background.

Conclusion

TypeScript provides an incredibly powerful toolkit for modeling sophisticated abstractions at the type level. By combining conditional types, mapped types, and variadic tuple types, we can encode rich constraints like currying that would be impossible in a less expressive type system.

But with great power comes great responsibility. It‘s easy to go overboard with type-level metaprogramming and create inscrutable, deeply nested types that are hard to reason about. As with any powerful tool, the key is to use it judiciously, in service of making your code more robust and maintainable.

Currying is a great example of a pattern that can be used selectively to great effect. By enabling partial application and point-free composition, currying can lead to more concise, reusable code. And with TypeScript‘s rich type system, we can model curried functions precisely, catching errors at compile-time that would be runtime failures in vanilla JavaScript.

So go forth and curry! With a little practice, you‘ll be cooking up type-safe, partially applicable functions in no time. Just remember to keep your types flavorful, not overly spicy. 😉

Similar Posts