Komplexe Typen
Komplexe Datentypen in TypeScript ermöglichen die präzise Modellierung vielfältiger Datenstrukturen und fördern die Typsicherheit in modernen Anwendungen. Durch den Einsatz von Arrays, Tupeln, Enums, Objekten sowie Union- und Intersection-Types lassen sich flexible und dennoch klar definierte Schnittstellen gestalten. Diese Typen unterstützen die Entwicklung skalierbarer, wartbarer und fehlerarmer Software, indem sie die Vorteile der statischen Typisierung gezielt auf komplexe Anwendungsfälle ausweiten.
Inhaltsverzeichnis
Arrays
Arrays sind eine der häufigsten Datenstrukturen. TypeScript bietet mehrere Wege, Arrays zu typisieren und sicherzustellen, dass sie nur die erwarteten Datentypen enthalten.
Array-Typ Syntax
Die Syntax für die Typisierung von Array sieht wie folgt aus.
let nums1: number[] = [1, 2, 3, 4, 5];
let nums2: Array<number> = [1, 2, 3, 4, 5];
Hier haben wir zwei gleichwertige Schreibweisen. Die erste Syntax (T[]
) ist häufiger und kompakter. Die zweite Syntax (Array<T>
) ist nützlich bei komplexen Typen.
Wichtigkeit von Typisierung
Warum ist eigentlich die Typisierung so wichtig? Wenn man sich ein Beispiel von einem Array in JavaScript anschaut, wird man feststellen, dass es durchaus möglich ist, Fehler zu produzieren, welche nicht sofort, sondern erst zur Laufzeit sich auswirken.
const prices = [10.99, 24.50, 5.00];
prices.push("29.99");
const total = prices.reduce((sum, price) => sum + price, 0);
console.log(total);
console.log(typeof total);
'40.4929.99'
'string'
Wie wir an diesem Beispiel sehen, konnten wir ohne Fehler einen String statt einer Zahl dem prices
Array hinzufügen. Das führt dazu, dass das Endergebnis falsch ist. Zusätzlich erhalten wir ein Ergebnis als ein String und nicht als eine Zahl.
Mit TypeScript würden wir diesen Fehler beim Kompilieren gemeldet kriegen.
const prices: number[] = [10.99, 24.50, 5.00];
prices.push("29.99"); // Error: Argument of type 'string' is not assignable
const total = prices.reduce((sum, price) => sum + price, 0);
Damit haben wir sichergestellt, dass nur Werte vom Typ number
im prices
Array enthalten sein können.
Array-Methoden und Type-Safety
TypeScript versteht, wie Array-Methoden Typen transformieren.
Typ-Transformation - map
Die Methode .map()
erzeugt aus jedem Array-Element einen neuen Wert. Das Resultat kann einen anderen Typ als das Ursprungs-Array haben.
const nums: number[] = [1, 2, 3, 4, 5];
// Wandelt number => string
const strings: string[] = nums.map(n => n.toString());
// Typ bleibt gleich
const doubled: number[] = nums.map(n => n * 2);
TypeScript erkennt automatisch den Rückgabetyp der Mapping-Funktion und erstellt daraus das Ergebnis-Array. So entsteht Type Safety: Das Ergebnis ist immer ein Array vom Typ des Rückgabewerts.
// bools ist vom Typ boolean[]
const bools = nums.map(n => n > 2);
Typ-Verfeinerung - filter
Die .filter()
Methode erstellt ein Teil-Array mit nur den Elementen, die den Filter passieren.
const nums: number[] = [1, 2, 3, 4, 5];
const evenNums = nums.filter(n => n % 2 === 0);
Da das Ursprungsarray nur number
enthält, bleibt auch das Ergebnis number[]
.
Manchmal hat man allerdings ein Array mit mehreren Typen. Hier ein Beispiel.
const mixed: (string | number)[] = [1, "Hello", 2, "world"];
Wenn man nun aus diesem Array nur Strings extrahieren wollen würde, könnte man sich eine Hilfs-Funktion schreiben, die eine spezielle Signatur verwendet.
const mixed: (string | number)[] = [1, "Hello", 2, "world"];
function isString(value: unknown): value is string {
return typeof value === "string";
}
const onlyStrings = mixed.filter(isString);
console.log(onlyStrings);
Die spezielle Signatur (Type Predicate) value is string
sagt TypeScript: “Wenn diese Funktion true
liefert, dann ist der Wert garantiert ein String”.
TypeScript erkennt, dass das Resultat nur noch Strings enthält - ohne zusätzlichen Cast oder Assertion.
Ohne Type Predicate würde TypeScript nicht erkennen, dass das Filter-Ergebnis ein reines String-Array ist, sondern (string | number)[]
als Wert annehmen.
Werte Aggregation - reduce
Die Methode .reduce()
berechnet einen einzelnen Wert aus allen Array-Elementen.
Mit der Angabe eines Startwerts für den Akkumulator, erkennt TypeScript den Typ des Ergebnisses.
const nums: number[] = [1, 2, 3, 4, 5];
const sum = nums.reduce((acc, cur) => acc + cur, 0);
console.log(typeof sum);
console.log(sum);
const concatenated = nums.reduce((acc, cur) => acc + cur.toString(), "");
console.log(typeof concatenated);
console.log(concatenated);
number
15
string
12345
Mehrdimensionale Arrays
Ein mehrdimensionales Array ist ein Array, dessen Elemente wiederum Arrays sind.
Typnotation in TypeScript
number[]
ist ein eindimensionales Array von Zahlennumber[][]
ist ein zweidimensionales Array - jedes Element ist wiederum einnumber[]
number[][][]
ist ein dreidimensionales Array
Hier ein Beispiel für die Deklaration und Initialisierung eines 2D-Arrays (Matrix).
let matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
Auf Elemente dieses Arrays würde man mit zwei Indizes zugreifen.
// matrix[zeile][spalte]
let element = matrix[1][2];
console.log(element); // 6
Nun ein Beispiel für die Deklaration und Initialisierung eines 3D-Arrays.
let cube: number[][][] = [
[
[1, 2],
[3, 4]
],
[
[5, 6],
[7, 8]
]
];
Um bei einem 3D-Array auf ein Element zuzugreifen, würde man mit drei Indizes arbeiten.
// cube[ebene][zeile][spalte]
let value = cube[1][0][1];
console.log(value); // 6
ReadonlyArray - Unveränderliche Arrays
Ein ReadonlyArray ist ein Array, das nicht verändert werden kann.
Das beudetet:
- keine Elemente hinzufügen
- keine Elemente entfernen
- keine Elemente verändern
Der Hauptnutzen besteht darin, die unbeabsichtigte Änderungen zu verhindern. Gerade in großen Anwendungen und bei der Übergabe von Arrays an Funktionen.
Syntax
Klassische Schreibweise: ReadonlyArray<T>
let readonlyNums: ReadonlyArray<number> = [1, 2, 3];
Kurzschreibweise: readonly T[]
let readonlyStrings: readonly string[] = ["a", "b", "c"];
Methoden
Bei ReadonlyArray sind nur die Methoden erlaubt, welche das Array nicht verändern.
Erlaubte Methoden: slice()
, map()
, filter()
, reduce()
, forEach()
, includes()
, find()
.
const upper = readonlyStrings.map(str => str.toUpperCase());
Verbotene Methoden: push()
, pop()
, shift()
, unshift()
, splice()
, sort()
, direkter Zugriff arr[0] = ...
.
readonlyStrings.push("d"); // ❌ Error
readonlyStrings.sort(); // ❌ Error
Praktisches Beispiel - Funktionsparameter schützen
Warum könnte es von Bedeutung sein? Wenn man Arrays an Funktionen übergibt, will man oft verhindern, dass sie innerhalb der Funktion verändert werden.
function processItems(items: readonly string[]): string {
// items.sort(); ❌ Fehler. sort() verändert das Array.
const sorted = [...items].sort(); // ✅ Neue Kopie
return sorted.join(", ");
}
In diesem Beispiel wurde items
als readonly string[]
deklariert. Somit sind alle Methoden, die items
verändern würden, nicht erlaubt. Nur durch Erstellung einer Kopie können neue oder veränderte Strukturen erzeugt werden.
Deep Readonly
Es besteht die Möglichkeit mit as const
Literal-Feature das Array und die Objekte darin so tief wie möglich readonly zu setzen.
Hier ein Beispiel, welches beide Möglichkeiten aufzeigt. Einmal die klassische, manuelle Zuweisung von readonly
für jede Property und jeden Wert und einmal die Verwendung von as const
.
const CONFIG_ONE: readonly [
{ readonly name: "debug", readonly value: true },
{ readonly name: "timeout", readonly value: 3000 }
] = [
{ name: "debug", value: true },
{ name: "timeout", value: 3000 }
];
// error TS2540: Cannot assign to 'name' because it is a read-only property.
CONFIG_ONE[0].name = "New value one";
const CONFIG_TWO = [
{ name: "debug", value: true },
{ name: "timeout", value: 3000 }
] as const;
// error TS2540: Cannot assign to 'name' because it is a read-only property.
CONFIG_TWO[0].name = "New value two";
Zu beachten ist, dass in JavaScript, wenn man diesen Code ohne Modifikationsversuch kompiliert, die Values dennoch veränderlich sind.
Tuples
Tuples sind Arrays mit einer festen Anzahl von Elementen spezifischer Typen in einer bestimmten Reihenfolge. Sie sind perfekt für Daten, die zusammengehören, aber unterschiedliche Typen haben.
Hier ein Beispiel.
// Zwei Zahlen, fester Typ und Reihenfolge
let point: [number, number] = [10, 20];
// String, Zahl, Boolean
let user: [string, number, boolean] = ["John", 30, true];
Der Unterschied zu einem Array ist, dass ein Array z.B. einfach number[]
oder string[]
ist - alle Elemente haben denselben Typ, wobei die Länge variieren kann.
Ein Tuple ist typsicher, kompakt und für strukturierte, kleine Datensätze mit unterschiedlicher Typenfolge ideal.
Zugriff auf Tuple-Elemente
Der TypeScript-Compiler gibt Fehler aus, wenn man auf nicht-existente Tuple-Positionen zugreift.
const point: [number, number] = [10, 20];
const x = point[0]; // Typ: number
const y = point[1]; // Typ: number
const z = point[2]; // ❌ Fehler
Destructing (Entpacken)
Das Entpacken funktioniert wie beim Array. Wichtig ist es darauf zu achten, dass die Reihenfolge korrekt beachtet wird.
let user: [string, number, boolean] = ["John", 30, true];
const [name, age, isActive] = user;
console.log(name);
console.log(age);
console.log(isActive);
John
30
true
Vorteile von Tuples
Es gibt ein paar gewichtige Vorteile, warum man in bestimmten Situationen Tuples verwenden sollte.
Mehr Typsicherheit und Lesbarkeit
function getCoordinatesObject(): { x: number, y: number } {
return { x: 10, y: 20 };
}
Der Rückgabewert in diesem Beispiel (ohne Tuples) ist ein Objekt. Man greift mit Namen auf die Felder zu.
function getCoordinatesObject(): [number, number] {
return [10, 20];
}
Hier ist der Rückgabewert ein kompaktes Datenpaket in fester Reihenfolge.
Mehrere Rückgabewerte
Mit Hilfe von Tuples kann man für eine Funktion mehrere Rückgabewerte definieren.
function divMod(dividend: number, divisor: number): [number, number] {
return [Math.floor(dividend / divisor), dividend % divisor];
}
const [quotient, remainder] = divMod(10, 3);
console.log(quotient); // 3
console.log(remainder); // 1
Man kann mehrere Werte kompakt und typisiert zurückgeben.
Benannte Tuples
Man kann Tuples mithilfe von type
benennen.
type Point3D = [x: number, y: number, z: number];
type UserRecord = [
id: string,
name: string,
age: number,
isActive: boolean
];
Man soll beachten, dass die Namen (x
, y
, z
) nur für Dokumentation und IDEs sind - in JavaScript bleiben Tuples positionsbasiert.
Rest-Elemente
Die Rest-Elemente in Tuples erlauben es Tuples mit einer variablen Länge von Elementen nach einer festen Reihenfolge (dem festen Part) zu definieren.
type StringNumberBooleans = [string, number, ...boolean[]];
const data1: StringNumberBooleans = ["Hello", 42]; // ✅ Ok
const data2: StringNumberBooleans = ["Hello", 42, true]; // ✅ Ok
const data3: StringNumberBooleans = ["Hello", 42, true, false]; // ✅ Ok
Die ersten beiden Elemente sind immer ein string
und ein number
, danach können beliebig viele Booleans folgen.
Hier ein praktisches Beispiel dazu.
type LogEntry = [
severity: "info" | "warn" | "error",
message: string,
...data: any[]
];
function log(...entry: LogEntry) {
const [severity, message, ...data] = entry;
console[severity](message, ...data);
}
log("info", "User logged in", { userId: 123 });
log("error", "Failed to connect", ["Fehler 1", "Fehler 2"], { retry : 3 });
User logged in { userId: 123 }
Failed to connect [ 'Fehler 1', 'Fehler 2' ] { retry: 3 }
Readonly Tuples
Mit readonly
kann man die Werte vor Veränderungen schützen.
Möglichkeit 1 - readonly
let readonlyPoint: readonly [number, number] = [10, 20];
// Änderungsversuch
readonlyPoint[0] = 30; // ❌ Error: TS2540: Cannot assign to '0' because it is a read-only property.
Möglichkeit 2 - Const Assertion
Mit as const
wird aus einem normalen Array ein readonly Tuple mit Literal-Typen. Die Werte sind nicht nur readonly, sondern behalten ihren exakten Wert.
let point = [10, 20] as const;
Hier ein praxisbezogenes Beispiel.
const DATABASE_CONFIG = [
"localhost",
5432,
"my_database",
"my_user",
"my_password"
] as const;
type DatabaseConfigType = typeof DATABASE_CONFIG;
console.log(DatabaseConfigType); // object
DATABASE_CONFIG[0] = "127.0.0.1"; // Fehler: TS2540: Cannot assign to '0' because it is a read-only property.
Objekte
Objekte sind das Herzstück von JavaScript und TypeScript. Das Typsystem bietet mächtige Werkzeuge, um Objekt-Strukturen präzise zu beschreiben.
In TypeScript kann man den Typ von Objekten explizit beschreiben. Das bedeutet, man sagt TypeScript, wie die Struktur eines Objekts aussieht - welche Properties es gibt und welchen Typ sie jeweils haben.
Damit bekommt man:
- Type Safety: Fehler werden frühzeitig erkannt.
- Autocomplete & IntelliSence: Der Editor kann bessere Unterstützung liefern.
- Dokumentation: Der Code ist besser lesbar und verständlicher.
Inline-Objekttypen
Hier ist die schnellste und direkteste Art, einen Objekttyp festzulegen.
let user = {
name: string,
age: number,
email: string
} = {
name: "John",
age: 30,
email: "john@example.com"
}
Erklärung
user
ist vom Typ Objekt mit drei Eigenschaften.
name
: Typ =string
age
: Typ =number
email
: Typ =string
Wenn man z.B. eine Property vergisst, falsche schreibt oder den falschen Typ verwendet, zeigt TypeScript einen Fehler an.
let user: {
name: string;
age: number;
email: string
} = {
name: "John",
age: "dreißig", // ❌ Error: string ist nicht erlaubt, number wird erwartet
email: "john@mail.com"
};
Optionale Properties (?)
Mit dem Fragezeichen hinter einer Property kann man sie optional machen. Sie kann gesetzt sein - muss aber nicht.
let product: {
id: string;
name: string;
description?: string; // Optional
price: number
} = {
id: "p12345",
name: "TypeScript Buch",
price: 29.99
};
In diesem Beispiel drüber ist description
optional. Beim Erstellen eines Objektes kann diese Eigenschaft fehlen. Wenn sie allerdings vorhanden ist, muss der Typ ein String sein.
Readonly Properties
Das readonly
Schlüsselwort macht eine Property schreibgeschützt. Man kann sie nur beim Erstellen des Objekts setzen, danach aber nicht mehr verändern.
let config: {
readonly apiUrl: string;
readonly maxRetries: number;
timeout: number;
} = {
apiUrl: "https://api.example.com",
maxRetries: 3,
timeout: 5000
};
config.timeout = 10000; // ✅ Ok - möglich
config.apiUrl = "new-url"; // ❌ Fehler - nicht zulässig
Das Schlüsselwort readonly
macht die Eigenschaft “immutable” (unveränderlich).
Index Signaturen
Index Signaturen erlaubt es, Objekttypen mit beliebigen, dynamischen Properties (Eigenschaften) zu definieren, deren Namen und Anzahl zur Entwicklungszeit noch nicht bekannt sind.
String Index Signatures - das klassische Dictionary
interface StringDictionary {
[key: string]: string;
}
Was passiert hier?
[key: string]
ist die Index Signatur- Sie erlaubt beliebige Strings als Property-Namen
- Jeder Wert dieser Eigenschaft muss vom Typ
string
sein
const translations: StringDictionary = {
hello: "Hallo",
goodbye: "Auf Wiedersehen"
};
Number Index Signatures - Arrays und Pseudo-Arrays
Number-Index-Signaturen erben die String-Index-Signaturen Regel, da JavaScript alle Objekt-Keys letztlich als Strings behandelt.
interface NumberArray {
[index: number]: string;
length: number;
}
Erklärung
[index: number]
erlaubt beliebige Zahlen als Index- Diese Zahlen stehen meist für Array-Indizes
- Jeder Wert muss vom Typ
string
sein - Es können auch feste Properties wie
length
vergeben werden
Gemischte Index Signatures und strukturierte Typen
Es ist möglich etwas dynamischere Objekt-Interfaces zu definieren, die sowohl feste als auch dynamische Typisierung beinhalten.
interface FlexibleData {
id: number; // Feste Typisierung
name: string; // Feste Typisierung
[key: string]: any; // Erlaubt weitere beliebige Eigenschaften
}
Die Index Signatur [key: string]: any
erweitert den Typ so, dass beliebige weitere Eigenschaften möglich sind.
Hier ein weiteres Beispiel mit echtem Objekt.
const data: FlexibleData = {
id: 1,
name: "John",
extra: "allowed",
another: 123,
nestd: { deep: true }
};
Wichtiger Hinweis: Die Typen der festen Eigenschaften müssen mindestens mit der Index Signatur kompatibel sein.
interface Broken {
id: number; // ❌ Konflikt (Fehler)
[key: string]: string;
}
In diesem Beispiel drüber müssen alle Eigenschaften eines künftigen Objekts, welches das Interface Broken
implementiert, vom Typ string
sein. Auch die explizit angegebenen. Hier versuchten wir eine Eigenschaft vom number
zu definieren. Das stellt einen Konflikt dar.
Eine mögliche Lösung wäre in diesem Fall entweder die Erweiterung auf any
oder auf string | number
.
interface Correct {
id: number; // ✅ Funktioniert
[key: string]: string | number;
}
Unterschied: String Index vs. Number Index Signatures
Es kann für die korrekte Definition von Objekt-Interfaces von Bedeutung sein, den Unterschied zwischen String- und Index-Signaturen zu verstehen.
String-Index
Definition: [key: string]
- Betrifft alle Eigenschaften des Objekts
- Alle Eigenschaften müssen dem angegebenen Typ entsprechen
- In JavaScript werden alle Property-Keys letztendlich als Strings behandelt
interface InterfaceOne {
id: number; // ❌ Konflikt
[key: string]: string;
}
Number-Index
Definition: [key: number]
- Betrifft nur numerische Indizes
- Benannte Eigenschaften sind davon ausgenommen
- Modelliert Array-ähnliche Strukturen
interface InterfaceTwo {
[index: number]: string | undefined;
length: number; // ✅ Funktioniert
}
Verschachtelte Objekte
In TypeScript können Objekt weitere Objekte oder Arrays als Properties (Eigenschaften) besitzen - also Verschachtelungen beliebiger Tiefe sind möglich.
Einfache Objekttypen
Hier ist Address
ein einfaches Objekt mit vier String-Eigenschaften.
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
Verschachtelte Objekte (Nested Objects)
Objekte können Properties besitzen, die wiederum Objekte sind, entweder als eigene Interfaces oder Inline-Typen.
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface Company {
name: string;
address: Address; // Nested object
}
Die Eigenschaft address
vom Typ Address
verweist auf ein weiteres Interface.
Man kann die Definition noch eine Ebene tiefer ausbauen.
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface Company {
name: string;
address: Address; // Verschachteltes Objekt
}
interface Person {
name: string;
age: number;
address: Address;
employer?: Company; // Optionales verschachteltes Objekt
}
Hier ein etwas ausführlicheres Beispiel mit einem Praxisbezug. Zuerst definieren wir das Interface.
interface ApiResponse<T> {
data: T;
meta: {
timestamp: string;
version: string;
pagination?: {
page: number;
pageSize: number;
total: number;
};
};
errors?: Array<{
code: string;
message: string;
field?: string;
}>
}
Erklärung
- Generics (
<T>
): Das Datenfeld ist flexibel - die Struktur hängt vom Anwendungsfall ab - meta: Enthält Informationen zur Antwort (z.B. Pagination) und kann selbst verschachtelte Objekte besitzen
- errors: Ein Array von Fehlerobjekten mit weiteren optionalen Feldern
Nun wenden wir dieses Interface an.
const response: ApiResponse<Person> = {
data: {
name: "John",
age: 30,
address: {
street: "Musterstraße 1",
city: "Berlin",
zipCode: "10115",
country: "DE"
},
employer: {
name: "Tech GmbH",
address: {
street: "Fabrikweg 3",
city: "Hamburg",
zipCode: "20095",
country: "DE"
}
}
},
meta: {
timestamp: "2024-06-21T22:23:00Z",
version: "1.2.0",
pagination: {
page: 1,
pageSize: 10,
total: 50
}
},
errors: [
{ code: "NOT_FOUND", message: "Keine weiteren Daten" }
]
};
Best Practices für komplexe Typen
Verwendung von Interfaces für Objekte
// ✅ Gut: Interface für Objekt-Strukturen
interface User {
id: number;
name: string;
}
// ❌ Weniger gut: Type Alias für einfache Objekte
type User = {
id: number;
name: string;
};
Readonly für Unveränderlichkeit
Man sollte nach Möglichkeit Daten vor unbeabsichtigten Änderungen schützen.
function processUser(user: Readonly<User>) {
// user.name = "New name"; // Error
}
// Für Änderungen Kopien erstellen
return { ...user, name: user.name.toUpperCase() };
Präzise Array-Typen
// Zu allgemein
let items: any[] = [];
// Besser: Spezifischer Typ
let userIds: string[] = [];
// Am besten: Readonly wenn möglich
const CONFIG_FLAGS: readonly string[] = ["debug", "verbose"];
Tuples für zusammengehörige Daten
// ❌ Mehrere separate Variablen
let lat = 52.520008;
let lng = 13.404954;
// ✅ Tuple für zusammengehörige Werte
let berlinCoords: [latitude: number, longitude: number] = [52.520008, 13.404954];