Ein Array in TypeScript ist nichts anderes als ein JavaScript-Array — mit einem zusätzlichen Etikett, das dem Compiler verrät, welcher Typ in der Liste steckt. Daraus entsteht der praktische Wert: Du kannst nicht versehentlich einen string in ein number[] legen, und beim Iterieren über die Liste kennt der Compiler den Element-Typ ohne Zutun. TypeScript bietet zwei gleichwertige Schreibweisen (T[] und Array<T>), einen separaten ReadonlyArray-Typ für Unveränderlichkeit und eine Reihe subtiler Unterschiede zwischen (string | number)[] und string[] | number[]. Dieser Artikel zeigt, wie du Arrays typsicher modellierst, was bei map/filter/reduce mit dem Typ passiert und warum Type Predicates beim Filtern unverzichtbar sind.
Was ein Array in TypeScript ist
Ein Array ist zur Laufzeit ein ganz normales JavaScript-Array — also ein Array-Objekt mit indizierten Elementen, einer length-Eigenschaft und allen vertrauten Methoden wie push, map oder filter. TypeScript fügt zur Compile-Zeit eine Typannotation für die Elemente hinzu. Aus „Liste von irgendwas" wird damit „Liste von number" oder „Liste von User".
const zahlen: number[] = [1, 2, 3, 4, 5];
const namen: string[] = ["Anna", "Boris", "Clara"];
// Compiler-Fehler: Argument vom Typ 'string' nicht zuweisbar an 'number'
// zahlen.push("sechs");
console.log(zahlen.length); // 5
console.log(namen[0]); // "Anna"5
AnnaWichtig zu wissen: Die Typprüfung passiert vor dem Kompilieren. Im erzeugten JavaScript steht kein Hinweis mehr auf den Element-Typ — Arrays haben keinen Laufzeit-Marker, der sagt „ich bin ein number[]". Das ist relevant für Type Guards: Du kannst zur Laufzeit prüfen, ob ein Wert ein Array ist (Array.isArray(x)), aber nicht, welcher Element-Typ darin steckt. Dafür musst du jedes Element einzeln untersuchen.
T[] vs. Array<T>
TypeScript akzeptiert zwei Schreibweisen für Array-Typen, und sie sind bedeutungsgleich. Der Compiler unterscheidet sie intern nicht — du wählst nach Lesbarkeit.
// Postfix-Notation (kompakter, am haeufigsten)
const a: number[] = [1, 2, 3];
const b: string[] = ["x", "y"];
// Generic-Notation (aequivalent)
const c: Array<number> = [1, 2, 3];
const d: Array<string> = ["x", "y"];Wann welche Form? Die Postfix-Variante T[] ist die Default-Wahl: kürzer, weniger Klammern, in Tutorials und Codebases die Mehrheit. Die Generic-Form Array<T> zeigt ihre Stärke bei komplexen Generic-Typen, in denen der eckige Klammer-Postfix mehrdeutig oder schwer lesbar wird.
// Mit komplexem Element-Typ: Generic-Form bleibt linear lesbar
const a: Array<Map<string, number>> = [];
const b: Map<string, number>[] = []; // funktioniert, weniger uebersichtlich
// Mit Union als Element-Typ
const c: Array<string | number> = ["a", 1, "b", 2];
const d: (string | number)[] = ["a", 1, "b", 2]; // Klammern noetig!
// In Generic-Constraints — Array<T> erzwingt die Generic-Form
function ersteHaelfte<T>(items: Array<T>): Array<T> {
return items.slice(0, Math.floor(items.length / 2));
}Teams legen das oft im Style-Guide fest. Einheitlichkeit ist hier wichtiger als die Wahl selbst — bleibe innerhalb eines Projekts bei einer Form.
Heterogene Arrays per Union
Arrays mit gemischten Element-Typen modellierst du über eine Union als Element-Typ. Achtung: (string | number)[] und string[] | number[] sind nicht dasselbe — der Unterschied ist subtil, aber praxisrelevant.
// Ein Array, das pro Element entweder string oder number enthält
const gemischt: (string | number)[] = ["a", 1, "b", 2];
// Entweder ein Array nur aus strings ODER ein Array nur aus numbers
let entwederOder: string[] | number[];
entwederOder = ["a", "b", "c"]; // ok
entwederOder = [1, 2, 3]; // ok
// entwederOder = ["a", 1]; // Fehler: weder rein string noch rein numberBeim Iterieren über (string | number)[] ist jedes Element vom Typ string | number — du musst pro Element narrowen. Beim Iterieren über string[] | number[] weiß der Compiler erst nach einem Narrowing der gesamten Variable, welcher der beiden Fälle vorliegt.
function laengeProElement(arr: (string | number)[]): number[] {
return arr.map(el => typeof el === "string" ? el.length : el);
}
console.log(laengeProElement(["hallo", 42, "x", 7]));[ 5, 42, 1, 7 ]Faustregel: Wenn pro Index unterschiedliche Typen erlaubt sein sollen, nimm die Union innerhalb des Arrays. Wenn die Liste als Ganzes entweder so oder so aussieht, nimm die Union um das Array herum.
Array-Methoden und Typ-Transformation
Die Standardmethoden map, filter, reduce und flatMap sind in TypeScript so typisiert, dass der Rückgabe-Element-Typ automatisch aus der Callback-Signatur abgeleitet wird. Du musst die Typen selten explizit nennen.
const zahlen: number[] = [1, 2, 3, 4];
// map: number -> string => string[]
const alsText = zahlen.map(n => `Zahl ${n}`);
// Typ: string[]
// filter: bleibt beim selben Element-Typ
const gerade = zahlen.filter(n => n % 2 === 0);
// Typ: number[]
// reduce: Accumulator-Typ aus dem Initial-Wert
const summe = zahlen.reduce((acc, n) => acc + n, 0);
// Typ: number
// flatMap: Element kann ein Array zurueckgeben, Ergebnis ist flach
const doppelt = zahlen.flatMap(n => [n, n]);
// Typ: number[]
console.log(alsText);
console.log(gerade, summe, doppelt);[ 'Zahl 1', 'Zahl 2', 'Zahl 3', 'Zahl 4' ]
[ 2, 4 ] 10 [ 1, 1, 2, 2, 3, 3, 4, 4 ]Bei reduce ist der Typ des Akkumulators nicht immer offensichtlich. Wenn der Initial-Wert leer ist oder vom Endtyp abweicht, hilft eine explizite Generic-Angabe — reduce<string[]>((acc, w) => [...acc, w], []) ist der saubere Weg, den Akkumulator-Typ festzulegen, ohne den Initial-Wert mit as zu casten.
Type Predicates für sicheres Filtern
filter hat einen Schönheitsfehler: Standardmäßig behält es den breiteren Element-Typ, auch wenn das Prädikat einen engeren Typ garantieren würde. Mit einem Type Predicate (x is T) sagst du dem Compiler explizit, welcher Typ nach dem Filter übrigbleibt.
const gemischt: (string | number)[] = ["a", 1, "b", 2, "c"];
// Ohne Predicate: nur strings durchgelassen, aber Typ bleibt (string | number)[]
const naiv = gemischt.filter(x => typeof x === "string");
// Typ: (string | number)[] — leider zu breit
// Mit Predicate: Compiler weiss, dass nur strings uebrig sind
const klar = gemischt.filter((x): x is string => typeof x === "string");
// Typ: string[]Praxis-Beispiel: Eine Liste mit möglicherweise null-Werten in einen Array ohne null überführen.
type User = { id: number; name: string };
const rohdaten: (User | null)[] = [
{ id: 1, name: "Anna" },
null,
{ id: 2, name: "Boris" },
null,
];
// Ohne Predicate: bleibt (User | null)[]
// Mit Predicate: wird User[]
const nurUser = rohdaten.filter((u): u is User => u !== null);
console.log(nurUser.length);
// nurUser[0].name ist jetzt sicher — kein null-Check noetig
console.log(nurUser[0].name);2
AnnaSeit TypeScript 5.5 kann der Compiler in vielen Fällen das Predicate selbst ableiten, wenn die Filter-Funktion eine boolean-Rückgabe mit einer einfachen Typ-Vergleichsbedingung ist. In älteren Versionen und bei komplexerer Logik brauchst du das explizite x is T weiterhin.
ReadonlyArray und readonly T[]
Manchmal willst du dem Compiler und den Leserinnen klar machen: „Diese Liste wird in dieser Funktion nicht verändert." Dafür gibt es ReadonlyArray<T> und die kurze Form readonly T[] — beide sind bedeutungsgleich.
function summiere(werte: ReadonlyArray<number>): number {
// werte.push(1); // Compiler-Fehler: 'push' existiert nicht
return werte.reduce((a, b) => a + b, 0);
}
function summiereKurz(werte: readonly number[]): number {
return werte.reduce((a, b) => a + b, 0);
}Auf einem readonly-Array sind alle mutierenden Methoden ausgeblendet: push, pop, shift, unshift, splice, sort, reverse, direktes Zuweisen per Index. Lese-Methoden wie slice, map, filter, concat, includes bleiben verfügbar — sie geben ohnehin neue Arrays oder Werte zurück.
| Aktion | number[] | readonly number[] |
|---|---|---|
Lesen per Index arr[0] | + | + |
slice, map, filter | + | + |
push, pop, splice | + | − |
arr[0] = 99 | + | − |
length lesen | + | + |
length zuweisen | + | − |
Wichtig — die Zuweisung ist einseitig: Ein number[] darf einem readonly number[] zugewiesen werden, umgekehrt nicht. Sonst könntest du dir die Read-Only-Garantie über einen Alias zerstören.
let x: readonly number[] = [];
let y: number[] = [];
x = y; // ok — schmaler Typ akzeptiert breiteren
// y = x; // Fehler: readonly-Array nicht an mutable zuweisbarReadonlyArray ist kein Wert, sondern nur ein Typ — new ReadonlyArray(...) gibt es nicht. Du erzeugst Read-Only-Listen, indem du normale Arrays als ReadonlyArray<T> annotierst oder mit as const einfrierst.
as const bei Arrays
Ohne Annotation wird ein Array-Literal breit inferiert: [1, 2, 3] ist number[]. Mit as const macht TypeScript daraus ein readonly Tupel mit Literal-Typen — eine ganz andere Qualität.
const a = [1, 2, 3];
// Typ: number[]
const b = [1, 2, 3] as const;
// Typ: readonly [1, 2, 3]
// b ist nicht nur readonly, sondern jedes Element ist ein Literal-Typ
type ErstesB = typeof b[0]; // 1
// Praxis: Whitelist als Tupel und daraus eine Union ableiten
const ROLLEN = ["admin", "editor", "viewer"] as const;
type Rolle = typeof ROLLEN[number]; // "admin" | "editor" | "viewer"
function pruefe(r: Rolle) { /* ... */ }
pruefe("admin"); // ok
// pruefe("gast"); // Fehler: nicht in der Union
console.log(ROLLEN);[ 'admin', 'editor', 'viewer' ]as const ist das idiomatische Mittel, um eine Wahrheit an einer Stelle (das Array-Literal) zu pflegen und daraus sowohl Laufzeit-Werte als auch eine exakte Union als Typ zu gewinnen. Mehr dazu im Artikel über readonly und as const.
Mehrdimensionale Arrays
Verschachtelte Arrays schreibst du, indem du die Postfix-Klammern stapelst. Ein number[][] ist ein Array, dessen Elemente selbst number[] sind.
const matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
// Spalten und Zeilen müssen nicht gleich lang sein
const jagged: number[][] = [
[1],
[2, 3],
[4, 5, 6],
];
console.log(matrix[1][2]); // 6
console.log(jagged[2][0]); // 46
4Wichtig — nicht mit Tupeln verwechseln: number[][] erlaubt beliebig viele Zeilen mit beliebig vielen Spalten. Wenn du eine feste Form (z. B. eine 3×3-Matrix oder ein Koordinatenpaar [number, number]) ausdrücken willst, brauchst du Tupel-Typen — siehe den separaten Artikel zu Tupeln.
Bei großen Datenmengen — Bildpuffern, Audio, numerischen Berechnungen — sind typisierte Arrays wie Float32Array oder Uint8Array performanter und speichereffizienter als gewöhnliche number[]. Sie haben in TypeScript eigene Typen und unterstützen den Großteil der Array-Methoden.
Array von Objekten
Der mit Abstand häufigste Praxis-Fall: eine Liste gleichförmiger Objekte — Benutzer, Bestellungen, Produkte. Definiere den Objekt-Typ als interface oder type und benutze ihn als Element-Typ.
interface User {
id: number;
name: string;
aktiv: boolean;
}
const benutzer: User[] = [
{ id: 1, name: "Anna", aktiv: true },
{ id: 2, name: "Boris", aktiv: false },
{ id: 3, name: "Clara", aktiv: true },
];
const aktive = benutzer.filter(u => u.aktiv);
const namen = benutzer.map(u => u.name);
console.log(aktive.length);
console.log(namen);2
[ 'Anna', 'Boris', 'Clara' ]Innerhalb von map/filter kennt der Compiler den Element-Typ ohne Zutun — u ist hier vom Typ User, IDE-Autovervollständigung funktioniert sofort. Auch bei verschachtelten Strukturen (etwa Bestellung[] mit jeweils Bestellposition[]) bleibt die Typsicherheit beliebig tief erhalten.
Tipp für Funktionen, die solche Listen entgegennehmen: Wenn der Aufrufer nicht erwarten muss, dass die Funktion die Liste verändert, nimm readonly User[] als Parameter-Typ. Das dokumentiert die Absicht und schützt vor versehentlichen sort()- oder push()-Aufrufen auf dem übergebenen Array.
FAQ
Soll ich T[] oder Array verwenden?
Beide sind identisch. Konvention in den meisten Codebases: T[] als Default, Array<T> nur dann, wenn der Element-Typ komplex genug ist, dass die Postfix-Form unleserlich wird (z. B. Array<Map<string, number>>). Lege dich pro Projekt fest und bleibe konsistent — über ESLint-Regeln wie @typescript-eslint/array-type automatisch erzwingbar.
Warum behält filter meinen breiten Typ?
Weil das Standard-Overload von filter keinen Bezug zwischen der booleschen Rückgabe und einem konkreten Typ herstellt — der Compiler kann nicht raten, dass typeof x === "string" einen string garantiert. Mit einem Type Predicate (x): x is string => ... machst du den Zusammenhang explizit und der Compiler kann den Rückgabetyp einengen.
Was ist der Unterschied zwischen (string | number)[] und string[] | number[]?
(string | number)[] ist ein Array, in dem jedes Element entweder string oder number sein darf — gemischt erlaubt. string[] | number[] ist entweder ein reines String-Array oder ein reines Number-Array, niemals gemischt. Beim Iterieren musst du im zweiten Fall die gesamte Variable narrowen, im ersten Fall jedes Element einzeln.
Macht readonly das Array zur Laufzeit unveränderlich?
Nein. readonly ist eine reine Compile-Zeit-Garantie. Im erzeugten JavaScript ist es ein ganz normales mutables Array — wer den Typ ignoriert oder per as umgeht, kann es trotzdem ändern. Für echte Laufzeit-Unveränderlichkeit brauchst du Object.freeze(arr).
Warum darf ich kein readonly number[] an number[] zuweisen?
Weil sonst die Read-Only-Garantie über einen Alias umgangen werden könnte: nach der Zuweisung an einen mutablen Alias dürfte plötzlich push aufgerufen werden — und das zerstört das Versprechen, das der ursprüngliche Typ gegeben hat. Die umgekehrte Richtung (number[] an readonly number[]) ist sicher, denn der Zugriff wird strenger, nicht lockerer.
Wann nehme ich Tupel statt Array?
Immer dann, wenn die Liste eine feste Länge und feste Position-zu-Typ-Zuordnung hat: Koordinaten [number, number], Schlüssel-Wert-Paare [string, V], React-State-Setter-Paare [T, (v: T) => void]. Wenn die Anzahl variabel ist oder alle Elemente denselben Typ haben, ist T[] richtig.
Wie inferiert TypeScript bei reduce?
Der Akkumulator-Typ wird aus dem Initial-Wert abgeleitet. Bei reduce((acc, n) => acc + n, 0) ist acc ein number. Bei reduce((acc, w) => acc.concat(w), []) wird der Initial-Wert [] als never[] erkannt — das knallt. Lösung: reduce<string[]>(...) mit expliziter Generic-Annotation, dann passt der Initial-Wert.
Was bringt as const bei einem Array?
Drei Dinge auf einmal: das Array wird readonly, es wird zu einem Tupel mit fester Länge, und seine Elemente werden zu Literal-Typen. Aus ["admin", "editor"] as const wird readonly ["admin", "editor"]. Über typeof ROLLEN[number] bekommst du daraus die Union "admin" | "editor" — eine Quelle für Wert und Typ.