🚀AdvancedAdvanced Design Patterns

Variadic Tuple Types

Use rest elements and spread in tuple types to model typed `compose`, `pipe`, `concat`, and argument-forwarding functions with full type inference.

Overview

**Variadic tuple types** (introduced in TypeScript 4.0) allow rest elements in tuple type positions and spreading of tuple types into other tuples. Before this feature, expressing the types of functions like `compose`, `pipe`, or `concat` required complex overloads. The key features are: - **Rest elements in tuples**: `type T = [string, ...number[], boolean]` - a string, any number of numbers, then a boolean - **Spread tuples**: `type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]` - **Labeled tuple elements**: `type Point = [x: number, y: number]` for better IDE support Variadic tuples make it possible to write a single, fully-typed `pipe` function that infers the correct type through N transformation steps, something that previously required writing 20+ overloads.

Key Points

  • •Rest elements in tuple positions: `[string, ...number[], boolean]`
  • •Spread in type position: `type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]`
  • •Leading and trailing rests are allowed (not just at the end)
  • •Labeled tuple elements: `[name: string, age: number]` - labels appear in IntelliSense
  • •Inference through spreads: TypeScript can infer T from `[...T, string]`
  • •`Parameters<F>` and `ReturnType<F>` leverage variadic tuples internally
  • •Enables: typed `pipe`/`compose`, `concat`, argument forwarding, partial application

Examples

Spreading and concatenating tuple types

// Concat two tuple types
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];

type AB = Concat<[string, number], [boolean, string]>;
// [string, number, boolean, string]

// Prepend an element
type Prepend<T, List extends unknown[]> = [T, ...List];
type P = Prepend<string, [number, boolean]>; // [string, number, boolean]

// Append an element
type Append<List extends unknown[], T> = [...List, T];
type A = Append<[number, boolean], string>; // [number, boolean, string]

// Drop the first element
type Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : never;
type T = Tail<[string, number, boolean]>; // [number, boolean]

// Drop the last element
type Init<T extends unknown[]> = T extends [...infer Rest, unknown] ? Rest : never;
type I = Init<[string, number, boolean]>; // [string, number]

// Zip two tuples together
type Zip<A extends unknown[], B extends unknown[]> = {
  [K in keyof A]: K extends keyof B ? [A[K], B[K]] : never;
};

type Z = Zip<[string, number], [boolean, Date]>;
// [[string, boolean], [number, Date]]

Typed argument forwarding and partial application

// Forward arguments from one function to another
function withLogging<Args extends unknown[], Return>(
  fn: (...args: Args) => Return,
  label: string
): (...args: Args) => Return {
  return (...args: Args) => {
    console.log(`[${label}] called with`, args);
    const result = fn(...args);
    console.log(`[${label}] returned`, result);
    return result;
  };
}

function add(a: number, b: number): number { return a + b; }
function greet(name: string, greeting: string): string {
  return `${greeting}, ${name}!`;
}

const loggedAdd = withLogging(add, 'add');
const loggedGreet = withLogging(greet, 'greet');

loggedAdd(2, 3);           // fully typed: (a: number, b: number) => number
loggedGreet('Alice', 'Hi'); // fully typed: (name: string, greeting: string) => string

// Partial application using variadic tuples
function partial<F extends (...args: any[]) => any, PartialArgs extends Partial<Parameters<F>>>(
  fn: F,
  ...partial: PartialArgs
): (...rest: Parameters<F> extends [...PartialArgs, ...infer R] ? R : never) => ReturnType<F> {
  return (...rest) => fn(...partial, ...rest);
}

const add5 = partial(add, 5);
console.log(add5(3)); // 8 - only needs the second argument

Typed compose and pipe using variadic tuples

// A type-safe pipe function for two steps
function pipe2<A, B, C>(
  f: (a: A) => B,
  g: (b: B) => C
): (a: A) => C {
  return (a) => g(f(a));
}

const processName = pipe2(
  (s: string) => s.trim(),
  (s: string) => s.toLowerCase()
);

console.log(processName('  ALICE  ')); // "alice"

// Typed concat for arrays using variadic tuples
function concatArrays<A extends unknown[], B extends unknown[]>(
  a: [...A],
  b: [...B]
): [...A, ...B] {
  return [...a, ...b] as [...A, ...B];
}

const nums   = [1, 2, 3] as const;
const bools  = [true, false] as const;
const combined = concatArrays(nums, bools);
// Type: [1, 2, 3, true, false] - exact literal tuple!

// Labeled tuples for better IDE experience
type NamedPoint = [x: number, y: number, z?: number];

function distance([x1, y1]: NamedPoint, [x2, y2]: NamedPoint): number {
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}

const origin: NamedPoint = [0, 0];
const point:  NamedPoint = [3, 4];
console.log(distance(origin, point)); // 5

Common Mistakes

  • Multiple rest elements in the same tuple: TypeScript allows only one rest element per tuple (e.g., `[...A, ...B]` as a spread of two tuples is fine, but `[...A[], ...B[]]` is not valid)
  • Using variadic tuples for runtime array operations without proper spread: `[...a, ...b]` at the type level is a type operation; at runtime you still need to spread values correctly
  • Overusing labeled tuple elements: labels are purely cosmetic (for IntelliSense display) and have no runtime effect or type enforcement - you can destructure them with any name

Note

Variadic tuples are most valuable for library code that wraps functions generically: middleware systems, argument transformers, decorator chains. For application code, the complexity is usually not justified. Start with explicit overloads for 1-5 cases; reach for variadic tuples when you genuinely need N-ary generics.

Exercises

Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What does `type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]` produce for `Concat<[string, number], [boolean]>`?