Ein Tupel ist ein Array mit fester Länge und einem exakten Typ pro Position. Während string[] eine beliebig lange Liste von Strings beschreibt, sagt [string, number] aus: genau zwei Elemente, an Index 0 ein String, an Index 1 eine Zahl. Der Compiler kennt diese Positionen und liefert exakte Typen beim Zugriff, beim Destructuring und in Funktions-Signaturen. Seit TypeScript 3.0 lassen sich Tupel als Rest-Parameter nutzen, seit 4.0 gibt es Labeled Tuples für lesbare Signaturen und Variadic Tuple Types für generische Komposition von Tupeln. Dieser Artikel zeigt die Syntax, die Grenzen — und die Stellen, an denen Tupel mehr leisten als ein Array.
Tupel vs. Array
Auf den ersten Blick sieht ein Tupel aus wie ein Array — und zur Laufzeit ist es eines. Der Unterschied liegt im Typsystem: Ein Array kennt nur einen Element-Typ und eine variable Länge. Ein Tupel kennt eine feste Länge und einen separaten Typ pro Index.
// Array: beliebige Anzahl, alle Elemente gleichen Typs
const namen: string[] = ["Anna", "Bob", "Carla"];
// Union-Array: beliebige Anzahl, Element ist string ODER number
const gemischt: (string | number)[] = ["Anna", 42, "Bob"];
// Tupel: genau zwei Elemente, Position 0 ist string, Position 1 ist number
const eintrag: [string, number] = ["Anna", 42];Der Unterschied zwischen [string, number] und (string | number)[] ist nicht kosmetisch — er entscheidet, ob der Compiler weiß, welcher Typ an welcher Position liegt.
const tupel: [string, number] = ["Anna", 42];
const a = tupel[0]; // Typ: string
const b = tupel[1]; // Typ: number
const union: (string | number)[] = ["Anna", 42];
const x = union[0]; // Typ: string | number — Compiler weiss nicht, was hier liegt
const y = union[1]; // Typ: string | numberEin Tupel ist also die richtige Wahl, wenn die Reihenfolge Bedeutung trägt und die Länge Teil des Vertrags ist — Koordinaten, Schlüssel-Wert-Paare, Rückgaben mit fester Struktur.
Basis-Syntax
Die Schreibweise ist eckige Klammern mit Komma-getrennten Typen:
type Punkt2D = [number, number];
type Eintrag = [string, number];
type Tripel = [string, number, boolean];
const p: Punkt2D = [3, 4];
const e: Eintrag = ["alter", 42];
const t: Tripel = ["aktiv", 1, true];
console.log(p[0], p[1]); // 3 4
console.log(e[0], e[1]); // alter 423 4
alter 42Beim Zugriff via Index kennt der Compiler den exakten Typ an der Position. Zugriffe jenseits der Länge sind ein Fehler — kein stilles undefined:
const paar: [string, number] = ["Anna", 42];
const name = paar[0]; // string
const alter = paar[1]; // number
// Fehler: Tuple type '[string, number]' of length '2' has no element at index '2'
const ueberlauf = paar[2];Destructuring funktioniert wie bei Arrays — die Typen werden positionsgenau übernommen:
function hashEintrag(eintrag: [string, number]) {
const [schluessel, wert] = eintrag;
// schluessel: string
// wert: number
return `${schluessel}=${wert}`;
}
hashEintrag(["alter", 42]);Labeled Tuples (seit 4.0)
Seit TypeScript 4.0 lassen sich Tupel-Elemente benennen. Die Labels sind reine Dokumentation — sie haben keinen Runtime-Effekt und erzwingen auch keine Variablen-Namen beim Destructuring. Was sie liefern: bessere Hover-Hints, sprechende Signature-Help in der IDE und Tupel, die sich wie Parameter-Listen lesen.
// Ohne Labels — funktional korrekt, aber nichts sagend
type Range = [number, number];
// Mit Labels — Intent steht in der Signatur
type RangeLabeled = [start: number, end: number];
function span(r: RangeLabeled): number {
// Beim Hover zeigt die IDE: r: [start: number, end: number]
const [start, end] = r;
return end - start;
}Regel: Entweder alle Elemente sind gelabelt oder keines. Mischen ist ein Compiler-Fehler.
// Fehler: Tuple members must all have names or all not have names
type Kaputt = [first: string, number];
// Korrekt:
type Ok = [first: string, second: number];Die Labels sind nicht an die Variablen-Namen beim Destructuring gebunden — du kannst sie ignorieren:
type Koordinate = [x: number, y: number];
function plot(p: Koordinate) {
const [a, b] = p; // a, b statt x, y ist erlaubt
// a: number, b: number
}Labels lohnen sich besonders bei Tupeln in Funktions-Signaturen und bei Rest-Parametern — also überall dort, wo die Position des Werts seine Bedeutung trägt.
Optional-Elemente
Mit ? markierst du Elemente am Ende des Tupels als optional. Sie dürfen weggelassen werden, und ihr Typ wird intern um undefined erweitert. Die length-Eigenschaft wird zur Union der möglichen Längen.
type Koordinate = [number, number, number?];
const ebene: Koordinate = [3, 4]; // ok, length: 2
const raum: Koordinate = [3, 4, 5]; // ok, length: 3
function dimension(k: Koordinate): number {
// k.length: 2 | 3
return k.length;
}
console.log(dimension(ebene)); // 2
console.log(dimension(raum)); // 32
3Beim Destructuring ist der optionale Wert number | undefined:
type Koordinate = [number, number, number?];
function lese(k: Koordinate) {
const [x, y, z] = k;
// z: number | undefined
if (z !== undefined) {
// hier ist z: number
}
}Wichtig: Optional-Elemente dürfen nur am Ende stehen. Eine Lücke in der Mitte ist nicht erlaubt — das wäre kein Tupel mehr, sondern ein Sparse-Konstrukt.
// Fehler: A required element cannot follow an optional element
type Kaputt = [number, string?, boolean];
// Korrekt:
type Ok = [number, boolean, string?];Rest-Elemente
Ein Rest-Element (...T[]) öffnet das Tupel für eine variable Anzahl gleichartiger Werte am Ende. Anders als bei Optional-Elementen bleibt die Position am Anfang fest typisiert.
type Kommando = [string, ...string[]];
const k1: Kommando = ["git"];
const k2: Kommando = ["git", "commit"];
const k3: Kommando = ["git", "commit", "-m", "fix"];
function ausfuehren([cmd, ...args]: Kommando) {
console.log(`Befehl: ${cmd}, Argumente: ${args.length}`);
}
ausfuehren(k1); // Befehl: git, Argumente: 0
ausfuehren(k3); // Befehl: git, Argumente: 3Befehl: git, Argumente: 0
Befehl: git, Argumente: 3Ein Tupel mit Rest-Element hat keine feste length — der Typ von length ist number, weil die Länge unbegrenzt ist.
Bis TypeScript 3.x durfte das Rest-Element nur am Ende stehen. Seit 4.0 darf es überall liegen — solange nur ein Rest-Element pro Tupel existiert.
// Vor 4.0: nur am Ende
type Klassisch = [string, ...number[]];
// Seit 4.0: auch in der Mitte oder am Anfang
type CsvZeile = [string, ...number[], boolean];
type LogEintrag = [...string[], number]; // Zeitstempel am Ende
const z: CsvZeile = ["header", 1, 2, 3, true];Genau ein Rest-Element pro Tupel — mehrere ... parallel sind ein Fehler.
Readonly-Tupel
Mit readonly machst du ein Tupel immutable. Index-Zuweisungen und mutierende Array-Methoden sind dann nicht mehr typsicher aufrufbar.
function distanz(p: readonly [number, number]): number {
const [x, y] = p;
// Fehler: Cannot assign to '0' because it is a read-only property
// p[0] = 99;
return Math.sqrt(x * x + y * y);
}
distanz([3, 4]); // 5as const erzeugt automatisch einen Readonly-Tupel-Typ mit Literal-Werten:
const a = [3, 4]; // Typ: number[]
const b = [3, 4] as const; // Typ: readonly [3, 4]
// Achtung: readonly ist nicht zu mutable Tupel zuweisbar
function brauchtMutable(p: [number, number]) {}
// Fehler: readonly [3, 4] ist nicht zuweisbar an [number, number]
// brauchtMutable(b);
// Wenn die Funktion nichts mutiert, sollte sie readonly akzeptieren:
function brauchtReadonly(p: readonly [number, number]) {}
brauchtReadonly(b); // okFaustregel für Bibliotheks-Code: Akzeptiere readonly an Parametern, gibt mutable Werte zurück, wenn der Aufrufer sie weiterverwenden soll. So bleibt die API für Aufrufer mit as const-Daten kompatibel.
Spread und Variadic Tuple Types
Seit TypeScript 4.0 lassen sich Tupel generisch zusammenstecken — Spreads innerhalb von Tupel-Typen dürfen jetzt Typ-Variablen sein. Das ist die Grundlage für robustes Funktions-Komposition ohne „Tod durch tausend Overloads".
type Arr = readonly unknown[];
// concat: [...T, ...U] verkettet zwei generische Tupel
function concat<T extends Arr, U extends Arr>(a: T, b: U): [...T, ...U] {
return [...a, ...b];
}
const ergebnis = concat([1, 2] as const, ["a", "b"] as const);
// ergebnis: [1, 2, "a", "b"]Ein klassisches Beispiel ist tail — gibt das Tupel ohne das erste Element zurück, mit exakter Längenreduktion im Typ:
function tail<T extends readonly unknown[]>(
arr: readonly [unknown, ...T]
): T {
const [, ...rest] = arr;
return rest as unknown as T;
}
const original = [1, 2, 3, 4] as const;
const ohneErstes = tail(original);
// ohneErstes: readonly [2, 3, 4]Partial Application wird damit zur Typ-Übung von wenigen Zeilen:
type Arr = readonly unknown[];
function partiell<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...kopf: T
) {
return (...rest: U) => f(...kopf, ...rest);
}
const log = (level: string, code: number, msg: string) =>
console.log(`[${level}/${code}] ${msg}`);
const warn = partiell(log, "WARN");
// warn: (code: number, msg: string) => void
warn(404, "not found");Diese Pattern waren vor 4.0 nur mit handgeschriebenen Overloads abbildbar — und brachen, sobald man die Argument-Anzahl änderte.
Tupel als Rest-Parameter
Schon seit TypeScript 3.0 kannst du ein Tupel als Typ für ...args einsetzen — die Funktion wird damit exakt wie eine Liste benannter Parameter typisiert.
function eintragen(...args: [name: string, alter: number]): string {
const [name, alter] = args;
return `${name} (${alter})`;
}
console.log(eintragen("Anna", 42));
// Fehler: Expected 2 arguments, but got 1
// console.log(eintragen("Anna"));
// Fehler: Expected 2 arguments, but got 3
// console.log(eintragen("Anna", 42, true));Anna (42)Der Spread eines Tupel-Werts in einen Funktions-Aufruf ist gleichermaßen typsicher:
function plot(x: number, y: number, label: string) {
console.log(`${label} @ (${x}, ${y})`);
}
const args: [number, number, string] = [3, 4, "P1"];
plot(...args); // ok — Tupel-Spread passt exakt zur SignaturDamit lassen sich höherwertige Funktionen schreiben, die Argument-Listen weiterreichen — ohne any und ohne Function.prototype.apply-Tricks.
Use Cases
1. React useState-Pattern — der Hook gibt ein Tupel zurück, das per Destructuring in Wert und Setter aufgeteilt wird. Der Tupel-Typ macht Reihenfolge und Anzahl unmissverständlich.
type StateHook<T> = [value: T, setValue: (next: T) => void];
function fakeUseState<T>(initial: T): StateHook<T> {
let v = initial;
return [v, (next) => { v = next; }];
}
const [name, setName] = fakeUseState("Anna");
// name: string, setName: (next: string) => void2. CSV-Zeilen mit fester Struktur — Header und numerische Werte exakt typisieren.
type Messung = [zeitstempel: string, ...werte: number[]];
const m: Messung = ["2026-05-15T12:00:00Z", 21.4, 22.1, 22.8];
const [zeit, ...zahlen] = m;
// zeit: string, zahlen: number[]3. Koordinaten und geometrische Werte — Position trägt Bedeutung, Länge ist Teil des Vertrags.
type Punkt2D = readonly [x: number, y: number];
type Punkt3D = readonly [x: number, y: number, z: number];
type Farbe = readonly [r: number, g: number, b: number, a?: number];
const rot: Farbe = [255, 0, 0];
const halbrot: Farbe = [255, 0, 0, 0.5];4. Result-Tupel statt Exceptions — Go-Style-Rückgaben, bei denen Fehler explizit zurückgegeben werden.
type Result<T> = [data: T, error: null] | [data: null, error: Error];
function parseJson(s: string): Result<unknown> {
try {
return [JSON.parse(s), null];
} catch (e) {
return [null, e as Error];
}
}
const [data, error] = parseJson("{ kaputt");
if (error) {
console.error(error.message);
} else {
console.log(data);
}| Pattern | Tupel-Form | Warum Tupel statt Objekt? |
|---|---|---|
| State-Hook | [value, setter] | Destructuring ohne Schlüssel, frei wählbare Namen |
| Result | [data, error] | Reihenfolge dokumentiert Konvention, kein Property-Lookup |
| Koordinate | [x, y, z?] | Math-Operationen iterieren über Indizes |
| CSV-Zeile | [header, ...werte] | Erste Spalte separat typisiert, Rest als Array |
| Range | [start: number, end: number] | Labels machen Reihenfolge selbsterklärend |
Häufige Stolperfallen
[string, number] ist nicht dasselbe wie (string | number)[].
Der Tupel-Typ kennt für jeden Index den exakten Typ — Position 0 ist string, Position 1 ist number. Der Union-Array hat an jeder Position string | number, also keine positionsabhängige Information. Wenn du nach x[0] einen string erwartest, brauchst du das Tupel.
push auf Tupel bricht die Längen-Garantie still.
TypeScript prüft push auf Tupeln nicht streng — du kannst ein [string, number] mit tuple.push("foo") erweitern, ohne dass der Compiler meckert. Die statische Länge bleibt 2, der Laufzeitwert hat aber 3 Elemente. Für echte Unveränderlichkeit nutze readonly — dann ist push verboten.
[1, 2] ohne as const ist number[], nicht [1, 2].
Array-Literale werden per Default zu Arrays geweitet — der Tupel-Typ entsteht nur mit expliziter Annotation oder as const. Wenn du Tupel-Inferenz brauchst (etwa für Rückgaben aus einer Funktion), schreibe entweder return [a, b] as const oder annotiere den Rückgabetyp explizit als Tupel.
Optional-Elemente nur am Ende.
[string, number?, boolean] ist ein Fehler — nach einem optionalen Element darf kein verpflichtendes mehr kommen. Wenn du mittendrin Lücken brauchst, ist ein Objekt mit optionalen Properties oder eine diskriminierte Union die richtige Form, nicht ein Tupel.
Nur ein Rest-Element pro Tupel.
[string, ...number[], ...boolean[]] ist nicht erlaubt — der Compiler könnte sonst nicht entscheiden, wo der eine Rest endet und der andere beginnt. Pro Tupel ein ..., beliebig viele feste Positionen davor und danach (seit 4.0).
Labeled Tuples sind reine Dokumentation.
Labels existieren nur im Typsystem — zur Laufzeit ist [start: number, end: number] ein Array [3, 9]. Du kannst Labels beim Destructuring frei umbenennen, sie erscheinen nicht in Property-Listen, und sie haben keinen Effekt auf Strukturelle Typ-Kompatibilität. Ihr Nutzen liegt in IDE-Hovern und Signature-Help.
React useState liefert ein Tupel — destructure es immer.
const state = useState(0) verliert die Positionsinformation: state wird zu [number, Dispatch<...>], und beim Zugriff via state[0] kannst du den Setter und den Wert verwechseln. Idiomatisch und typsicher: const [count, setCount] = useState(0). Labels in der Hook-Signatur machen den Vertrag explizit.
Mixed Tuple [string, ...number[], boolean] braucht TS 4.0+.
Rest-Elemente in der Mitte oder am Anfang eines Tupels sind ein 4.0-Feature (Variadic Tuple Types). Mit älteren Compiler-Zielen bekommst du einen Fehler. Wer Bibliotheks-Typen mit weiter Kompatibilität schreibt, sollte das im Hinterkopf behalten und stattdessen einen klassischen Tail-Rest [A, B, ...C[]] wählen.
Weiterführende Ressourcen
Externe Quellen
- Tuple Types – TypeScript Handbook
- TypeScript 4.0 Release Notes (Variadic, Labeled)
- TypeScript 3.0 Release Notes (Tuple Rest)