Variadic Tuple Types

Spreading Tuples in Types

Variadic tuple types ([...A, ...B]) let generic code work with argument lists of any length while keeping every element type. They power typed concat, pipe, bind and middleware helpers.

Example

function concat<A extends unknown[], B extends unknown[]>(a: [...A], b: [...B]): [...A, ...B] {
  return [...a, ...b];
}

function partial<Fixed extends unknown[], Rest extends unknown[], R>(
  fn: (...args: [...Fixed, ...Rest]) => R,
  ...fixed: Fixed
): (...rest: Rest) => R {
  return (...rest) => fn(...fixed, ...rest);
}

const t = concat(["OD-1", 2], [true]); // [string, number, boolean]
console.log(t, typeof t[1]);

function price(currency: string, gstPercent: number, amount: number): string {
  return `${currency} ${(amount * (1 + gstPercent / 100)).toFixed(2)}`;
}
const inrWithGst = partial(price, "INR", 18); // (amount: number) => string
console.log(inrWithGst(1000), inrWithGst(250));

Output

[ 'OD-1', 2, true ] number
INR 1180.00 INR 295.00

Wrapping Functions Without Losing Types

Example

A generic wrapper keeps the exact parameter list of the wrapped function. Calling the timed version with wrong arguments is still an error.

function withLogging<Args extends unknown[], R>(name: string, fn: (...args: Args) => R): (...args: Args) => R {
  return (...args) => {
    const result = fn(...args);
    console.log(`${name}(${args.map((a) => JSON.stringify(a)).join(", ")}) = ${JSON.stringify(result)}`);
    return result;
  };
}

const addToCart = withLogging("addToCart", (sku: string, qty: number) => ({ sku, qty, ok: qty <= 5 }));
addToCart("SKU-1", 2);
addToCart("SKU-2", 9);

Output

addToCart("SKU-1", 2) = {"sku":"SKU-1","qty":2,"ok":true}
addToCart("SKU-2", 9) = {"sku":"SKU-2","qty":9,"ok":false}