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.
Inhaltsverzeichnis
- 1. Was Variadic Tuple Types sind und warum sie 2020 kamen
- 2. Grundsyntax: Spread an beliebiger Tupel-Position
- 3. Praxisbeispiel: Typsicheres Currying
- 4. Praxisbeispiel: Generisches Tupel-Concat, Head und Tail
- 5. Kombination mit Rest-Parametern in Funktionssignaturen
- 6. Labeled Tuple Elements für lesbare Signaturen
- 7. Praxisbeispiel: Typsicherer Event-Emitter
- 8. Grenzen und Debugging der Tupel-Inferenz
- 9. Variadic Tuples im Vergleich zu Alternativen
- 10. Zusammenfassung
- 11. FAQ
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.