Variadic Tuple Types praktisch nutzen: Currying, Concat und Events
AI generated
<T>
type
TypeScript · Typsystem · Advanced Types
Variadic Tuple Types praktisch nutzen
von typsicherem Currying bis zum Event-Emitter

Variadic Tuple Types erlauben Spread-Operatoren an beliebiger Position innerhalb eines Tupel-Typs, nicht nur am Ende. Das macht Funktions-Signaturen für Currying, generisches Tupel-Concat und typsichere Event-Emitter möglich, die vorher nur mit vielen manuellen Überladungen oder mit any zu erreichen waren. Dieser Artikel zeigt die Syntax, drei Praxisbeispiele und die Grenzen der Tupel-Inferenz.

17 Min. Lesezeit Variadic Tuples · Rest-Parameter · Currying TypeScript 4.0+

1. Was Variadic Tuple Types sind und warum sie 2020 kamen

Variadic Tuple Types wurden mit TypeScript 4.0 eingeführt und erlauben es, den Spread-Operator ...T an beliebiger Position innerhalb eines Tupel-Typs zu verwenden, nicht mehr nur am Ende wie bei klassischen Rest-Parametern. Vor dieser Erweiterung mussten Funktionen mit variabler Argumentanzahl entweder auf any[] zurückgreifen oder eine lange Kette manueller Überladungen pflegen, um jede mögliche Argumentkombination einzeln zu typisieren.

Der praktische Auslöser für Variadic Tuple Types war die Bibliothek Redux und ähnliche Utility-Funktionen wie compose, curry und concat, bei denen die Anzahl und Reihenfolge der Argumente sich direkt auf den Rückgabetyp auswirkt. Ohne generische Tupel-Manipulation konnten solche Funktionen nur mit Verlust an Typsicherheit oder mit unpraktikabel vielen Überladungen abgebildet werden. Seit TypeScript 4.0 lassen sich diese Muster mit wenigen Zeilen generischem Code exakt typisieren.

Dieser Artikel führt von der Grundsyntax über drei konkrete Anwendungsfälle, Currying, Tupel-Concat und Event-Emitter, bis zu den Grenzen der Tupel-Inferenz. Jedes Beispiel ist direkt im TypeScript-Playground nachvollziehbar und zeigt, wie Variadic Tuple Types in echtem Produktionscode eingesetzt werden.

2. Grundsyntax: Spread an beliebiger Tupel-Position

Die Kernidee von Variadic Tuple Types ist einfach: Ein generischer Tupel-Parameter, der mit extends unknown[] oder extends readonly unknown[] eingeschränkt ist, kann per Spread ...T in ein neues Tupel eingebettet werden, egal ob am Anfang, in der Mitte oder am Ende. TypeScript behält dabei die exakte Länge und die exakten Elementtypen des ursprünglichen Tupels bei, statt sie zu einem einzigen Array-Typ zu verallgemeinern.

Diese Eigenschaft unterscheidet Variadic Tuple Types fundamental von einem einfachen T[]. Ein Prepend<Head, Tail>-Typ etwa fügt ein Element vorne an ein bestehendes Tupel an, ohne die Typinformationen der übrigen Elemente zu verlieren, was mit einem generischen Array niemals möglich wäre, weil dort jede Positionsinformation nach der Zusammenführung verschwindet.


// 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. Praxisbeispiel: Typsicheres Currying

Currying wandelt eine Funktion mit mehreren Parametern in eine Kette von Funktionen mit je einem Parameter um. Ohne Variadic Tuple Types ließ sich der Rückgabetyp einer Curry-Funktion nur mit Function-Overloads für jede feste Parameteranzahl exakt abbilden, was für Funktionen mit vier oder fünf Parametern schnell unübersichtlich wurde. Mit generischen Tupeln reicht eine einzige rekursive Definition, die für beliebig viele Parameter funktioniert.

Der Schlüssel liegt darin, die Parameterliste als Tupel über Parameters<F> zu extrahieren und dieses Tupel dann Element für Element abzubauen, während bei jedem Schritt ein neuer Funktionstyp mit dem verbleibenden Rest der Parameter zurückgegeben wird. Dieses Muster kombiniert Variadic Tuple Types direkt mit der im vorherigen Artikel behandelten Rekursion über Conditional Types.


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. Praxisbeispiel: Generisches Tupel-Concat, Head und Tail

Eine typsichere concat-Funktion für Tupel ist ein weiteres Standardbeispiel, das ohne Variadic Tuple Types nicht möglich war. Die Funktion soll zwei Tupel unterschiedlicher Länge und unterschiedlicher Elementtypen entgegennehmen und ein neues Tupel zurückgeben, das exakt die Vereinigung beider Element-Sequenzen abbildet, nicht nur einen generalisierten Array-Typ wie (A | B)[].

Ergänzend dazu lassen sich Head- und Tail-Typen definieren, die das erste Element beziehungsweise die restlichen Elemente eines Tupels extrahieren. Diese drei Bausteine, Concat, Head und Tail, bilden zusammen die Grundlage für die meisten fortgeschrittenen Tupel-Utility-Typen, einschließlich des im vorherigen Abschnitt gezeigten Currying.


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. Kombination mit Rest-Parametern in Funktionssignaturen

Variadic Tuple Types entfalten ihre größte praktische Wirkung in Kombination mit Rest-Parametern echter Funktionssignaturen, nicht nur in reinen Typ-Definitionen. Eine Funktion wie bind<T, A extends unknown[], B extends unknown[], R> kann damit exakt beschreiben, welche Argumente bereits gebunden sind und welche beim Aufruf noch fehlen, indem die ursprüngliche Parameterliste in einen gebundenen und einen verbleibenden Teil aufgeteilt wird.

Auch Wrapper-Funktionen, die einer beliebigen Zielfunktion Logging oder Timing hinzufügen, profitieren davon: Der Wrapper kann die komplette Parameterliste der Zielfunktion per ...args: Parameters<F> entgegennehmen und unverändert weiterreichen, ohne die Typinformation der einzelnen Parameter zu verlieren. Das war vor Variadic Tuple Types nur mit any[] oder mit expliziten Überladungen für jede Funktionsarität möglich.


// 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 für lesbare Signaturen

Labeled Tuple Elements, ebenfalls seit TypeScript 4.0 verfügbar, ergänzen Variadic Tuple Types um lesbare Namen für einzelne Tupel-Positionen, ohne die zugrunde liegende Struktur zu verändern. Statt [string, number, boolean] schreibt man [name: string, age: number, active: boolean], was in der IDE als Parameter-Hinweis erscheint und die generierten .d.ts-Dateien erheblich lesbarer macht.

Besonders in Kombination mit Rest-Parametern zahlt sich das aus: [first: string, ...rest: number[]] zeigt sofort, dass das erste Element eine besondere Rolle spielt, während der Rest eine homogene Liste ist. Bibliotheken, die viele generische Utility-Funktionen mit Variadic Tuple Types exportieren, nutzen Labels systematisch, um die automatisch generierte Dokumentation nutzbar zu halten.


// 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. Praxisbeispiel: Typsicherer Event-Emitter

Ein Event-Emitter, bei dem jeder Event-Name eine eigene, feste Argumentliste hat, ist ein Paradebeispiel für die Kombination aus Variadic Tuple Types und Mapped Types. Statt einer generischen emit(event: string, ...args: any[])-Signatur beschreibt eine Event-Map jeden Event-Namen als Schlüssel und die zugehörige Argumentliste als Tupel-Wert. Der Emitter selbst nutzt dann einen generischen Parameter, der über keyof an die konkrete Event-Map gebunden ist.

Der entscheidende Vorteil gegenüber einer nicht-typisierten Lösung: Ruft man emit("user:created", user) auf, prüft der Compiler, ob die übergebenen Argumente exakt der für "user:created" definierten Tupel-Signatur entsprechen. Ein vertauschtes Argument oder ein fehlender Parameter wird sofort als Compile-Fehler gemeldet, nicht erst zur Laufzeit in Produktion.


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. Grenzen und Debugging der Tupel-Inferenz

Variadic Tuple Types stoßen an Grenzen, sobald mehrere generische Rest-Elemente in derselben Position kombiniert werden sollen, etwa [...T, ...U], bei dem sowohl T als auch U zur Compile-Zeit unbekannte Länge haben. TypeScript kann in solchen Fällen die genaue Aufteilung nicht immer eindeutig auflösen und fällt auf eine gröbere Typinferenz zurück, die zwar korrekt, aber weniger präzise ist als bei einem einzelnen offenen Rest-Element.

Ein häufiger Stolperstein beim Debugging ist der Unterschied zwischen normalen Arrays und as const-Tupeln als Funktionsargumente. Übergibt man ein Array-Literal ohne as const, weitet TypeScript die einzelnen Elementtypen oft zu einem allgemeineren Typ auf, etwa number[] statt eines exakten Tupels, was die Variadic-Tuple-Inferenz in nachgelagerten generischen Funktionen unbrauchbar macht. Explizite Tupel-Typannotationen oder as const am Aufrufort lösen dieses Problem zuverlässig.


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 im Vergleich zu Alternativen

Für variable Argumentlisten stehen in TypeScript mehrere Ansätze zur Verfügung, die sich in Präzision, Lesbarkeit und Wartungsaufwand deutlich unterscheiden. Die folgende Tabelle vergleicht die gängigen Alternativen für typische Anwendungsfälle.

Ansatz Präzision Wartungsaufwand Wann geeignet
...args: any[] Keine Gering Nie in neuem Code empfehlenswert
Manuelle Overloads pro Arität Hoch Sehr hoch Nur bei sehr wenigen festen Ariteten
Variadic Tuple Types Hoch Gering Currying, Concat, Event-Maps, Wrapper
Einzelnes Objekt-Argument Hoch Gering Wenn Reihenfolge keine Rolle spielen soll

In der Praxis ersetzen Variadic Tuple Types heute fast alle Fälle, in denen früher Overload-Ketten oder any[]-Rest-Parameter genutzt wurden. Ein einziges Objekt-Argument bleibt dort vorzuziehen, wo die Argumentreihenfolge für Aufrufer keinen erkennbaren Sinn ergibt, etwa bei mehr als drei optionalen Parametern gleichen Typs.

Mironsoft

TypeScript-Architektur, Typsystem-Beratung und Refactoring

Overload-Ketten statt eleganter Tupel-Typen?

Wir ersetzen unübersichtliche Function-Overloads durch Variadic Tuple Types, bauen typsichere Event-Emitter und Curry-Utilities und schulen eure Teams im Umgang mit generischen Tupeln.

API-Refactoring

Overload-Ketten durch generische Variadic-Tuple-Signaturen ersetzen

Event-Systeme

Typsichere Event-Emitter mit Event-Maps für eure Frontend-Architektur

Team-Schulung

Workshops zu Generics, Tupel-Manipulation und modernem Typsystem-Design

10. Zusammenfassung

Variadic Tuple Types lösen ein Problem, das vor TypeScript 4.0 nur mit Kompromissen bei Typsicherheit oder Wartbarkeit lösbar war: Funktionssignaturen mit variabler Argumentanzahl, bei denen jede Position einen eigenen, konkreten Typ trägt. Der Spread-Operator ...T an beliebiger Tupel-Position, kombiniert mit Parameters<F> und rekursiven Conditional Types, macht Currying, generisches Concat und typsichere Event-Emitter mit wenigen Zeilen Code möglich.

Labeled Tuple Elements verbessern zusätzlich die Lesbarkeit generierter Typdefinitionen, ohne die zugrunde liegende Struktur zu verändern. Die Hauptgrenze liegt bei mehreren offenen Rest-Elementen in derselben Signatur und beim Verlust der exakten Tupelform durch fehlendes as const. Wer diese Stolpersteine kennt, kann Variadic Tuple Types gezielt dort einsetzen, wo bisher Overload-Ketten oder any[] nötig waren.

Variadic Tuple Types — Das Wichtigste auf einen Blick

Grundprinzip

Seit TypeScript 4.0 kann ...T an beliebiger Position eines Tupel-Typs stehen, nicht nur am Ende, ohne die exakte Elementstruktur zu verlieren.

Typische Anwendungen

Currying, generisches Tupel-Concat, Partial Application und typsichere Event-Emitter mit Event-Maps.

Labeled Elements

Namen für Tupel-Positionen wie [x: number, y: number] verbessern IDE-Hinweise und generierte Deklarationsdateien.

Häufige Falle

Ohne as const weitet TypeScript Array-Literale auf, wodurch die exakte Tupelform für nachgelagerte Generics verloren geht.

11. FAQ: Variadic Tuple Types praktisch nutzen

1Was sind Variadic Tuple Types?
Tupel-Typen mit Spread-Operator an beliebiger Position, nicht nur am Ende. Seit TypeScript 4.0 verfügbar, ermöglichen exakte Typisierung variabler Argumentlisten.
2Unterschied zu Rest-Parametern?
Klassische Rest-Parameter erlauben Spread nur am Ende. Variadic Tuple Types erlauben Spread überall im Tupel bei exakter Typerhaltung.
3Wie funktioniert typsicheres Currying?
Parameters<F> extrahiert die Parameterliste als Tupel, rekursiv wird Element für Element abgebaut, bis keine Parameter mehr übrig sind.
4Array-Literal wird nicht als Tupel erkannt?
Ohne as const weitet TypeScript auf einen generalisierten Array-Typ auf. as const oder explizite Tupel-Annotation behält die exakte Struktur.
5Was sind Labeled Tuple Elements?
Benannte Tupel-Positionen wie [x: number, y: number]. Ändern die Struktur nicht, verbessern aber IDE-Hinweise und Deklarationsdateien.
6Zwei generische Rest-Tupel kombinieren?
Nur eingeschränkt möglich. Mit zwei unbekannten Längen kann TypeScript die Aufteilung nicht immer eindeutig auflösen.
7Typsicherer Event-Emitter?
Über eine Event-Map mit Event-Name als Schlüssel und Argument-Tupel als Wert, gebunden über keyof an emit- und on-Methoden.
8Ersetzen sie Function-Overloads?
In den meisten Fällen ja, eine einzige generische Definition deckt beliebig viele Argumentkombinationen ab.
9Wann ein Objekt-Argument statt Tupel?
Wenn die Reihenfolge keinen erkennbaren Sinn ergibt, etwa bei vielen optionalen Parametern gleichen Typs.
10Funktionieren sie mit readonly Tupeln?
Ja, readonly unknown[] als Constraint funktioniert genauso und lässt sich ebenso per Spread verarbeiten.