Variadic Tuple Types in Practice: Currying, Concat, and Events
AI generated
<T>
type
TypeScript · Type System · Advanced Types
Variadic Tuple Types in Practice
from typesafe currying to a real event emitter

Variadic tuple types allow the spread operator at any position inside a tuple type, not only at the end. This makes function signatures for currying, generic tuple concat, and typesafe event emitters possible, something that previously required many manual overloads or a fallback to any. This article shows the syntax, three practical examples, and the boundaries of tuple inference.

17 min read Variadic Tuples · Rest Parameters · Currying TypeScript 4.0+

1. What variadic tuple types are and why they arrived in 2020

Variadic tuple types were introduced in TypeScript 4.0 and allow the spread operator ...T to be used at any position inside a tuple type, no longer only at the end as with classic rest parameters. Before this addition, functions with a variable number of arguments either had to fall back to any[] or maintain a long chain of manual overloads to type every possible argument combination individually.

The practical trigger for variadic tuple types was Redux and similar utility functions such as compose, curry, and concat, where the number and order of arguments directly affects the return type. Without generic tuple manipulation, such functions could only be modeled with a loss of type safety or with an impractical number of overloads. Since TypeScript 4.0, these patterns can be typed exactly with a few lines of generic code.

This article walks from the basic syntax through three concrete use cases, currying, tuple concat, and event emitters, to the limits of tuple inference. Every example can be tried directly in the TypeScript playground and shows how variadic tuple types are used in real production code.

2. Basic syntax: spread at any tuple position

The core idea of variadic tuple types is simple: a generic tuple parameter constrained with extends unknown[] or extends readonly unknown[] can be embedded into a new tuple via spread ...T, whether at the start, in the middle, or at the end. TypeScript preserves the exact length and exact element types of the original tuple, instead of generalizing them into a single array type.

This property fundamentally distinguishes variadic tuple types from a simple T[]. A Prepend<Head, Tail> type, for example, adds an element to the front of an existing tuple without losing the type information of the remaining elements, which would never be possible with a generic array, because there every positional information disappears after merging.


// Spread at the start: prepend an element while keeping exact tuple shape
type Prepend<Head, Tail extends unknown[]> = [Head, ...Tail];

type A = Prepend<string, [number, boolean]>; // [string, number, boolean]

// Spread at the end: append an element
type Append<Tail extends unknown[], Item> = [...Tail, Item];

type B = Append<[number, boolean], string>; // [number, boolean, string]

// Spread in the middle: insert between two fixed elements
type InsertMiddle<Before extends unknown[], Item, After extends unknown[]> =
  [...Before, Item, ...After];

type C = InsertMiddle<[string], boolean, [number]>; // [string, boolean, number]

3. Practical example: typesafe currying

Currying transforms a function with multiple parameters into a chain of functions with one parameter each. Without variadic tuple types, the return type of a curry function could only be modeled exactly using function overloads for every fixed parameter count, which quickly became unwieldy for functions with four or five parameters. With generic tuples, a single recursive definition is enough, one that works for an arbitrary number of parameters.

The key is to extract the parameter list as a tuple via Parameters<F> and then break down this tuple element by element, returning a new function type with the remaining rest of the parameters at every step. This pattern combines variadic tuple types directly with the recursion over conditional types covered in the previous article.


type Curry<F extends (...args: unknown[]) => unknown> =
  Parameters<F> extends [infer First, ...infer Rest]
    ? (arg: First) => Rest extends [] ? ReturnType<F> : Curry<(...args: Rest) => ReturnType<F>>
    : ReturnType<F>;

function addThree(a: number, b: number, c: number): number {
  return a + b + c;
}

declare const curriedAdd: Curry<typeof addThree>;

const result = curriedAdd(1)(2)(3); // number, fully type-checked at every step
// curriedAdd(1)("x"); // Error: argument of type string not assignable to number

4. Practical example: generic tuple concat, head, and tail

A typesafe concat function for tuples is another standard example that was not possible without variadic tuple types. The function should accept two tuples of different length and different element types and return a new tuple that exactly reflects the union of both element sequences, not just a generalized array type like (A | B)[].

In addition, Head and Tail types can be defined that extract the first element and the remaining elements of a tuple respectively. These three building blocks, concat, head, and tail, together form the foundation for most advanced tuple utility types, including the currying shown in the previous section.


function concat<T extends unknown[], U extends unknown[]>(
  first: [...T],
  second: [...U]
): [...T, ...U] {
  return [...first, ...second];
}

const merged = concat([1, 2] as const, ["a", "b", "c"] as const);
// readonly [1, 2, "a", "b", "c"] — exact tuple shape preserved

type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : [];

type H = Head<[string, number, boolean]>; // string
type T2 = Tail<[string, number, boolean]>; // [number, boolean]

5. Combining with rest parameters in function signatures

Variadic tuple types show their greatest practical impact in combination with rest parameters of real function signatures, not just in pure type definitions. A function like bind<T, A extends unknown[], B extends unknown[], R> can exactly describe which arguments are already bound and which are still missing at call time, by splitting the original parameter list into a bound and a remaining part.

Wrapper functions that add logging or timing to an arbitrary target function also benefit: the wrapper can accept the entire parameter list of the target function via ...args: Parameters<F> and forward it unchanged, without losing the type information of the individual parameters. Before variadic tuple types, this was only possible with any[] or with explicit overloads for every function arity.


// Partial application: split parameters into "already bound" and "remaining"
function partial<A extends unknown[], B extends unknown[], R>(
  fn: (...args: [...A, ...B]) => R,
  ...boundArgs: A
): (...rest: B) => R {
  return (...rest: B) => fn(...boundArgs, ...rest);
}

function formatPrice(currency: string, locale: string, amount: number): string {
  return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount);
}

const formatEur = partial(formatPrice, "EUR", "de-DE");
formatEur(1299.5); // "1.299,50 €" — remaining parameter stays fully typed

6. Labeled tuple elements for readable signatures

Labeled tuple elements, also available since TypeScript 4.0, complement variadic tuple types with readable names for individual tuple positions, without changing the underlying structure. Instead of [string, number, boolean], you write [name: string, age: number, active: boolean], which appears as a parameter hint in the IDE and makes generated .d.ts files considerably more readable.

This pays off especially in combination with rest parameters: [first: string, ...rest: number[]] immediately shows that the first element plays a special role while the rest is a homogeneous list. Libraries that export many generic utility functions with variadic tuple types use labels systematically to keep the automatically generated documentation usable.


// Without labels: what do these positions mean?
type Point3DUnlabeled = [number, number, number];

// With labels: self-documenting in IDE tooltips and generated .d.ts files
type Point3D = [x: number, y: number, z: number];

function move(...[dx, dy, dz]: [dx: number, dy: number, dz: number]): void {
  console.log(`Moving by ${dx}, ${dy}, ${dz}`);
}

// Labeled rest element combined with a fixed first element
type LogArgs = [level: "info" | "warn" | "error", ...messages: string[]];

7. Practical example: a typesafe event emitter

An event emitter where every event name has its own fixed argument list is a textbook example for combining variadic tuple types with mapped types. Instead of a generic emit(event: string, ...args: any[]) signature, an event map describes every event name as a key and the associated argument list as a tuple value. The emitter itself then uses a generic parameter bound to the concrete event map via keyof.

The decisive advantage over an untyped solution: calling emit("user:created", user) makes the compiler check whether the passed arguments exactly match the tuple signature defined for "user:created". A swapped argument or a missing parameter is reported immediately as a compile error, not only at runtime in production.


interface EventMap {
  "user:created": [user: { id: number; name: string }];
  "user:deleted": [userId: number, reason: string];
  "connection:closed": []; // no arguments at all
}

class TypedEmitter<Events extends Record<string, unknown[]>> {
  private listeners: { [K in keyof Events]?: ((...args: Events[K]) => void)[] } = {};

  on<K extends keyof Events>(event: K, listener: (...args: Events[K]) => void): void {
    (this.listeners[event] ??= []).push(listener);
  }

  emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
    this.listeners[event]?.forEach((listener) => listener(...args));
  }
}

const emitter = new TypedEmitter<EventMap>();
emitter.on("user:deleted", (userId, reason) => console.log(userId, reason));
emitter.emit("user:deleted", 42, "policy violation"); // fully checked at compile time

8. Limits and debugging of tuple inference

Variadic tuple types reach their limits once several generic rest elements need to be combined in the same position, for example [...T, ...U], where both T and U have an unknown length at compile time. In such cases TypeScript cannot always resolve the exact split unambiguously and falls back to a coarser type inference that is correct but less precise than with a single open rest element.

A common pitfall when debugging is the difference between plain arrays and as const tuples as function arguments. If you pass an array literal without as const, TypeScript often widens the individual element types to a more general type, such as number[] instead of an exact tuple, which renders variadic tuple inference in downstream generic functions useless. Explicit tuple type annotations or as const at the call site reliably solve this problem.


function firstTwo<T extends unknown[]>(items: [...T]): T {
  return items;
}

// Without "as const": TypeScript widens to number[], losing exact tuple shape
const widened = firstTwo([1, 2, 3]); // inferred as number[]

// With "as const": exact tuple [1, 2, 3] is preserved for downstream inference
const exact = firstTwo([1, 2, 3] as const); // inferred as readonly [1, 2, 3]

9. Variadic tuples compared to alternatives

TypeScript offers several approaches for variable argument lists that differ significantly in precision, readability, and maintenance effort. The following table compares the common alternatives for typical use cases.

Approach Precision Maintenance effort When suitable
...args: any[] None Low Never recommended in new code
Manual overloads per arity High Very high Only for very few fixed arities
Variadic tuple types High Low Currying, concat, event maps, wrappers
Single object argument High Low When order should not matter

In practice, variadic tuple types today replace almost every case where overload chains or any[] rest parameters used to be used. A single object argument remains preferable where the argument order does not make recognizable sense to the caller, for example with more than three optional parameters of the same type.

Mironsoft

TypeScript architecture, type system consulting, and refactoring

Overload chains instead of elegant tuple types?

We replace unwieldy function overloads with variadic tuple types, build typesafe event emitters and curry utilities, and train your teams to work with generic tuples.

API refactoring

Replace overload chains with generic variadic tuple signatures

Event systems

Typesafe event emitters with event maps for your frontend architecture

Team training

Workshops on generics, tuple manipulation, and modern type system design

10. Summary

Variadic tuple types solve a problem that before TypeScript 4.0 could only be solved with compromises on type safety or maintainability: function signatures with a variable number of arguments where every position carries its own concrete type. The spread operator ...T at any tuple position, combined with Parameters<F> and recursive conditional types, makes currying, generic concat, and typesafe event emitters possible with just a few lines of code.

Labeled tuple elements additionally improve the readability of generated type definitions without changing the underlying structure. The main limit lies with multiple open rest elements in the same signature and with losing the exact tuple shape due to a missing as const. Knowing these pitfalls lets you apply variadic tuple types exactly where overload chains or any[] used to be necessary.

Variadic Tuple Types — the essentials at a glance

Core principle

Since TypeScript 4.0, ...T can appear at any position of a tuple type, not only at the end, without losing the exact element structure.

Typical applications

Currying, generic tuple concat, partial application, and typesafe event emitters with event maps.

Labeled elements

Names for tuple positions like [x: number, y: number] improve IDE hints and generated declaration files.

Common pitfall

Without as const, TypeScript widens array literals, losing the exact tuple shape needed for downstream generics.

11. FAQ: Variadic Tuple Types in Practice

1What are variadic tuple types?
Tuple types with a spread operator at any position, not only at the end. Available since TypeScript 4.0 for exact typing of variable argument lists.
2Difference from rest parameters?
Classic rest parameters only allow spread at the end. Variadic tuple types allow spread anywhere in the tuple while preserving exact types.
3How does typesafe currying work?
Parameters<F> extracts the parameter list as a tuple, recursively broken down element by element until none remain.
4Array literal not recognized as a tuple?
Without as const TypeScript widens to a generalized array type. as const or an explicit tuple annotation preserves the exact structure.
5What are labeled tuple elements?
Named tuple positions like [x: number, y: number]. They do not change the structure but improve IDE hints and declaration files.
6Combining two generic rest tuples?
Only to a limited extent, with two unknown lengths TypeScript cannot always resolve the split unambiguously.
7Typesafe event emitter?
Through an event map with event name as key and argument tuple as value, bound via keyof to emit and on methods.
8Do they replace function overloads?
In most cases yes, a single generic definition covers an arbitrary number of argument combinations.
9When use an object argument instead?
When order does not make recognizable sense, for example with many optional parameters of the same type.
10Do they work with readonly tuples?
Yes, readonly unknown[] as a constraint works the same and can equally be processed via spread.