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.

ts
// 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.

ts
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 | number

Ein 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:

ts
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 42
Output
3 4
alter 42

Beim Zugriff via Index kennt der Compiler den exakten Typ an der Position. Zugriffe jenseits der Länge sind ein Fehler — kein stilles undefined:

ts
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:

ts
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.

ts
// 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.

ts
// 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:

ts
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.

ts
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));  // 3
Output
2
3

Beim Destructuring ist der optionale Wert number | undefined:

ts
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.

ts
// 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.

ts
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: 3
Output
Befehl: git, Argumente: 0
Befehl: git, Argumente: 3

Ein 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.

ts
// 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.

ts
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]); // 5

as const erzeugt automatisch einen Readonly-Tupel-Typ mit Literal-Werten:

ts
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); // ok

Faustregel 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".

ts
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:

ts
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:

ts
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.

ts
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));
Output
Anna (42)

Der Spread eines Tupel-Werts in einen Funktions-Aufruf ist gleichermaßen typsicher:

ts
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 Signatur

Damit 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.

ts
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) => void

2. CSV-Zeilen mit fester Struktur — Header und numerische Werte exakt typisieren.

ts
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.

ts
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.

ts
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);
}
PatternTupel-FormWarum 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

/ Weiter

Zurück zu Komplexe Typen

Zur Übersicht