Ein TypeScript-Tupel ist ein Array mit einer festen Anzahl von Elementen, bei dem jede Position ihren eigenen Typ hat. [string, number] bedeutet genau zwei Elemente: zuerst ein String, dann eine Zahl. Du schreibst die Typen in eckigen Klammern in der Reihenfolge, in der die Werte stehen.
Zur Laufzeit ist ein Tupel ein einfaches JavaScript-Array. Alles, was ein Tupel hinzufügt (die feste Länge und der Typ an jeder Position), prüft der Compiler und entfernt es dann.
Tupel-Syntax
| Tupel-Typ | Akzeptiert | Typ von length |
|---|---|---|
[string, number] | genau einen String, dann eine Zahl | 2 |
[x: number, y: number] | dasselbe, mit Bezeichnern zur besseren Lesbarkeit | 2 |
[number, number, number?] | 2 oder 3 Zahlen | 2 | 3 |
[string, ...number[]] | einen String, dann beliebig viele Zahlen | number |
[...string[], number] | beliebig viele Strings, dann eine Zahl | number |
readonly [number, number] | ein Paar, das nicht verändert werden kann | 2 |
[] | nur ein leeres Array | 0 |
Jede Form wird unten erklärt. Beachte den Typ von length: Bei einem festen Tupel ist er ein Literaltyp, der Compiler weiß also, dass pair.length genau 2 ist.
Was der Compiler prüft
Ein Tupel-Typ legt die Anzahl der Elemente, ihre Reihenfolge und den Typ an jeder Position fest. Ist davon etwas falsch, gibt es einen Compilerfehler:
index.ts(2,7): error TS2322: Type '[string]' is not assignable to type '[string, number]'.
Source has 1 element(s) but target requires 2.
index.ts(3,36): error TS2322: Type 'number' is not assignable to type 'string'.
index.ts(3,40): error TS2322: Type 'string' is not assignable to type 'number'.
index.ts(5,16): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.
Ein einfaches Array könnte den letzten Fehler nie finden: Bei string[] ist arr[2] schlicht ein string, der zur Laufzeit zufällig undefined ist.
Benannte Tupel-Elemente
Bezeichner dokumentieren, was jede Position bedeutet. Sie ändern nichts am Typ oder am Indexzugriff, aber Editoren zeigen sie in Hovern und Signaturhinweisen, was [number, number] viel weniger rätselhaft macht.
Seit TypeScript 5.2 kannst du manche Positionen benennen und andere nicht, etwa [first: string, number]. Bezeichner sind nur für Leser da: [x: number, y: number] und [number, number] sind derselbe Typ und untereinander zuweisbar.
Optionale Elemente
Ein ? hinter einem Elementtyp macht diese Position optional. Optionale Elemente müssen nach den Pflichtelementen stehen, und jedes erweitert den Typ von length.
Ein optionales Element zu lesen ergibt T | undefined, daher brauchst du vor Rechnungen einen Standardwert im Destructuring-Muster (a = 1) oder eine Prüfung.
Rest-Elemente
Ein Rest-Element, ...T[], steht für beliebig viele Elemente vom Typ T. Es kann am Ende, am Anfang oder in der Mitte stehen, höchstens eines pro Tupel.
Die length eines Tupels mit Rest-Element ist number, weil die Größe nicht mehr fest ist. Fest bleibt, wo die typisierten Positionen liegen.
Readonly Tupel und as const
readonly [T, U] entfernt push, pop, splice und Zuweisungen per Index, und genau so sollte ein Wert fester Länge sein. as const hinter einem Array-Literal leitet ein readonly Tupel aus Literaltypen ab.
(typeof SIZES)[number] macht aus dem Tupel eine Union seiner Elementtypen, ein Muster, das unter Indexed Access Types erklärt wird. Ein readonly Tupel kann nicht an einen Parameter übergeben werden, der als veränderbares Tupel typisiert ist, deshalb sollten Funktionen, die nur lesen, readonly [number, number] akzeptieren.
Die readonly-Prüfung gilt nur beim Kompilieren. Zur Laufzeit ist das Array nicht eingefroren (die Zuweisung oben lief tatsächlich, wie die Ausgabe zeigt), nimm also Object.freeze, wenn du eine Garantie zur Laufzeit brauchst.
Ein Tupel aus einer Funktion zurückgeben
Mehrere Werte als Tupel zurückzugeben ist das Prinzip von Reacts useState (const [value, setValue] = useState(0)). Der Haken: Ein Array-Literal in einem return wird als Array abgeleitet, nicht als Tupel.
index.ts(9,13): error TS2365: Operator '+' cannot be applied to types 'number | (() => number)' and 'number'.
index.ts(10,1): error TS2349: This expression is not callable.
Not all constituents of type 'number | (() => number)' are callable.
Type 'number' has no call signatures.
Die Funktion gibt (number | (() => number))[] zurück, daher bekommen beide Namen beim Destructuring den Union-Typ. Es gibt zwei Lösungen: den Rückgabetyp annotieren oder as const ergänzen.
Mit einem Tupel als Rückgabewert können Aufrufer die Teile beliebig benennen. Bei mehr als zwei oder drei Werten, oder wenn die Reihenfolge nicht offensichtlich ist, gib lieber ein Objekt zurück: { count, increment } dokumentiert sich selbst.
Tupel als Funktionsparameter
Ein Rest-Parameter, der als Tupel typisiert ist, beschreibt eine ganze Argumentliste, optionale Argumente eingeschlossen. So stellt der eingebaute Utility-Typ Parameters<T> die Parameter einer Funktion dar.
Wird ein Tupel in einen Aufruf gespreadet, prüft TypeScript jedes Argument nach Position, was bei einem Spread von (string | number)[] nicht ginge.
Tupel vs Array
Array (string | number)[] | Tupel [string, number] | |
|---|---|---|
| Länge | beliebig | fest (oder begrenzt durch optionale und Rest-Elemente) |
Typ von x[0] | string | number | string |
Typ von x[5] | string | number | Compilerfehler TS2493 |
Typ von length | number | 2 |
| Reihenfolge der Typen | nicht erfasst | erfasst |
| Wert zur Laufzeit | JavaScript-Array | dasselbe JavaScript-Array |
| Typische Verwendung | Listen gleichartiger Elemente | kleine feste Gruppen: Paare, Koordinaten, [key, value], mehrere Rückgabewerte |
Tupel tauchen auch in eingebauten Typen auf. Object.entries(obj) gibt [string, T][] zurück, und eine Map wird aus [key, value]-Tupeln erzeugt:
Eine Falle: Ein veränderbares Tupel hat weiterhin alle Array-Methoden, also kompiliert pair.push(3) auf einem [string, number] und erzeugt stillschweigend ein Array mit drei Elementen, dessen Typ zwei behauptet. Tupel als readonly zu deklarieren schließt diese Lücke. Und weil Typen entfernt werden, werden Daten von außerhalb des Programms (JSON, eine API) zur Laufzeit nicht gegen einen Tupel-Typ geprüft: Validiere Länge und Elementtypen, bevor du ihnen vertraust.
Variadic Tuple Types
Tupel-Typen können andere Tupel-Typen spreaden, [...T, ...U]. Zusammen mit Generics lassen sich so Funktionen typisieren, die verketten oder vorn anfügen und dabei jede Position behalten:
Auch Bibliothekstypen stützen sich auf Tupel-Inferenz: Promise.all([fetchUser(), fetchPosts()]) wird zu einem Tupel mit einem Typ je Eingabe-Promise aufgelöst.
Häufig gestellte Fragen
Was ist ein Tupel in TypeScript?
Ein Tupel ist ein Array-Typ mit fester Länge, bei dem jede Position ihren eigenen Typ hat: [string, number] sind genau zwei Elemente, erst ein String, dann eine Zahl. Zur Laufzeit ist es ein gewöhnliches JavaScript-Array; die Länge und die Typen je Position werden nur beim Kompilieren geprüft.
Was ist der Unterschied zwischen Tupel und Array in TypeScript?
Ein Array-Typ wie (string | number)[] hat eine beliebige Länge, und jedes Element hat denselben (Union-)Typ, also ist arr[0] vom Typ string | number. Bei einem Tupel wie [string, number] ist die Länge bekannt, t[0] ist string, t[1] ist number, und t[2] ist ein Compilerfehler.
Wie gebe ich in TypeScript ein Tupel aus einer Funktion zurück?
Annotiere den Rückgabetyp, function f(): [number, string], oder beende den Rückgabeausdruck mit as const, was ein readonly Tupel ergibt. Ohne beides wird return [count, setCount] als Array einer Union abgeleitet, etwa (number | (() => void))[], und Destructuring liefert Union-Typen.
Was sind benannte Tupel-Elemente?
Bezeichner für die Positionen, [name: string, age: number]. Sie ändern weder den Typ noch den Zugriff (weiterhin t[0]), aber Editoren zeigen sie in Hovern und in den Parameterhinweisen von Funktionen, deren Parameter als Tupel typisiert sind. Optionale und Rest-Elemente funktionieren mit Bezeichnern: [x: number, y?: number], [head: string, ...rest: number[]].
Kann man in TypeScript per push etwas zu einem Tupel hinzufügen?
Bei einem veränderbaren Tupel ja: push kompiliert, weil Tupel die Array-Methoden erben, obwohl es die feste Länge bricht. Deklarierst du das Tupel als readonly (oder erzeugst es mit as const), werden push, pop und Zuweisungen per Index zu Compilerfehlern.