Interfaces
Interfaces in TypeScript ermöglichen die klare Definition und Strukturierung von Objekttypen. Sie fördern die Wiederverwendbarkeit, Lesbarkeit und Wartbarkeit von Code, indem sie Verträge für die Form von Daten festlegen. Durch den Einsatz von Interfaces lassen sich komplexe Anwendungen typsicher und flexibel gestalten.
Inhaltsverzeichnis
Einführung
Interfaces sind das Herzstück von TypeScript’s Typ-System. Sie definieren Contracts (Verträge/Abkommen) für die Struktur von Objekten, Klassen und Funktionen. Im Gegensatz zu Klassen existieren Interfaces nur zur Compile-Zeit und haben keinen Einfluß auf den generierten JavaScript-Code.
Schlüssel-Eigenschaften
- Nur zur Compile-Zeit: Interfaces existieren nur während der TypeScript-Entwicklung und werden im finalen JavaScript-Code entfernt. Sie erzeugen keinen Laufzeit-Overhead.
- Strukturelle Typisierung: TypeScript prüft nicht den Namen des Typs, sondern dessen Struktur (“Duck Typing”).
- Erweiterbar: Interfaces können durch Vererbung, mehrere Interfaces oder “Declaration Merging” kombiniert werden.
- Bessere Fehlermeldungen: Bei Abweichungen vom Interface erhält man exakte Compiler-Fehlermeldungen mit Angabe des Problems.
interface I_Person {
username: string;
active: boolean;
age?: number;
}
Erklärung des Beispiels
Dieses Interface I_Person
legt fest, dass ein Person-Objekt mindestens username
und active
als Pflichtfelder vom Typ string
und boolean
haben muss. Das Feld age
ist optional (?
).
Optionale Properties
Properties können als optional markiert werden, indem ein ?
nach dem Property-Namen gesetzt wird. TypeScript prüft, ob die Property existiert. Fehlt sie, ist das kein Fehler - existiert sie, muss sie zum definierten Typ passen.
interface I_UserSettings {
theme: string;
fontSize: number;
darkMode?: boolean;
notifications?: {
email: boolean;
push: boolean;
}
}
const settings: I_UserSettings = {
theme: "dark",
fontSize: 14
}
In diesem Beispiel gilt Folgendes:
theme
undfontSize
sind verpflichtenddarkMode
undnotifications
können weggelassen werden
Readonly Properties
Properties können als schreibgeschützt markiert werden. Dies ist dann sinnvoll, wenn man sicherstellen möchte, dass bestimmte Werte nicht verändert werden sollen. Eignet sich hervorragend beispielsweise für Konfigurationsobjekte.
interface I_ImmutablePoint {
readonly x: number;
readonly y: number;
}
const point: I_ImmutablePoint = { x: 10, y: 20 };
Man kann beim Erstellen des Objekts die Felder x
und y
setzen, danach nie wieder ändern. Ein Versuch, diese Werte zu ändern, führt zu einem Compile-Fehler.
point.x = 50; // ❌ Cannot assign to 'x' because it is a read-only property.
Methoden in Interfaces
Interfaces können Methoden-Signaturen definieren - das heißt, man bestimmt, welche Methoden mit welchem Argument- und Rückgabetyp existieren müssen (oder optional ?
existieren können).
Es gibt zwei Schreibweisen:
- Methoden-Signatur
- Property mit Funktionstyp
interface I_Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
multiply?(a: number, b: number): number;
}
const basicCalc: I_Calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
}
console.log(basicCalc.add(4, 5));
console.log(basicCalc.subtract(10, 3));
9
7
Alternative Schreibweise.
interface I_Calculator {
add: (a: number, b: number) => number;
subtract: (a: number, b: number) => number;
}
In den beiden Beispielen müssen die Methoden add
und subtract
implementiert werden. Die Methode multiply
ist optional. Sie kann, muss aber nicht definiert werden.
Index Signatures
Mit Index Signatures können Interfaces so definiert werden, dass sie beliebige weitere Properties mit bestimmten Typen zulassen. Das ist für Dictionary-ähnliche Strukturen oder flexible Objekte mit dynamischen Keys.
interface I_StringDictionary {
[key: string]: string;
}
const sampleDict: I_StringDictionary = {
name: "John",
email: "john@mail.com"
};
In diesem Beispiel kann das Objekt vom Typ I_StringDictionary
beliebige Eigenschaften haben, welche Werte vom Typ string
haben müssen.
Man kann auch eine Mischung aus bekannten Properties (explizit definierte Properties) und dynamischen Properties definieren.
interface I_HybridDictionary {
id: number;
[key: string]: string | number;
}
const sampleObject: I_HybridDictionary = {
id: 1,
username: "john",
email: "john@mail.com"
};
Interface Vererbung
Interfaces können andere Interfaces erweitern (extends
). Mehrere Interfaces können gleichzeitig erweitert werden (Mehrfachvererbung). Das Kind-Interface erbt alle Properties und Methoden der Eltern.
interface I_Animal {
name: string;
age: number;
}
interface I_Dog extends I_Animal {
breed: string;
bark(): void;
}
const myDog: I_Dog = {
name: "Rex",
age: 3,
breed: "Labrador",
bark: () => console.log("Woof")
};
Hier ist ein Beispiel für Mehrfachvererbung.
interface I_Identifiable {
id: string;
}
interface I_Timestamped {
createdAt: Date;
}
interface I_User extends I_Identifiable, I_Timestamped {
username: string;
}
const userOne: I_User = {
id: 1,
createdAt: new Date(),
username: "John"
};
Funktions-Interfaces
Interfaces können Funktionstypen beschreiben. Das ist praktisch, um Funktionsobjekte mit vordefinierter Signatur zu typisieren.
interface I_SearchFunction {
(source: string, subString: string): boolean;
}
const mySearch: I_SearchFunction = (src, sub) => {
return src.includes(sub);
};
Das Interface I_SearchFunction
beschreibt die Signatur: Zwei Strings als Parameter, Rückgabewert muss ein Boolean sein. Eine zugewiesene Funktion muss genau dieser Signatur entsprechen.
Interfaces mit Klassen
Eine Klasse kann ein oder mehrere Interfaces implementieren (implements
). Die Klasse muss alle Eigenschaften und Methoden aus dem Interface bereitstellen.
interface I_ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements I_ClockInterface {
currentTime: Date = new Date();
setTime(d: Date): void {
this.currentTime = d;
}
anotherFunc(): void {
console.log("Do nothing");
}
}
const clockObject = new Clock();
clockObject.setTime(new Date());
clockObject.anotherFunc();
Die Klasse Clock
garantiert, dass sie currentTime
als Property und die Methode setTime
wie im Interface besitzt. Jede Abweichung führt zu einem Typ-Fehler.
Generische Interfaces
Interfaces können generisch sein, also Typ-Parameter akzeptieren (<T>
). Damit werden Interfaces sehr flexibel und können für viele Typen wiederverwendet werden.
interface I_User {
id: string;
name: string;
email: string;
}
interface I_Repository<T> {
findById(id: string): T | undefined;
save(entity: T): void;
delete(id: string): boolean;
}
class UserRepository implements I_Repository<I_User> {
private users: I_User[] = [];
findById(id: string): I_User | undefined {
return this.users.find(user => user.id === id);
}
save(user: I_User): void {
const existingUserIndex = this.users.findIndex(u => u.id === user.id);
if (existingUserIndex >= 0) {
// Aktualisiere den Benutzer
this.users[existingUserIndex] = user;
} else {
// Neuen Benutzer hinzufügen
this.users.push(user);
}
}
delete(id: string): boolean {
const initialLength = this.users.length;
this.users = this.users.filter(u => u.id !== id);
return this.users.length !== initialLength;
}
// Zusätzliche spezifische Methode für UserRepository
findByEmail(email: string): I_User | undefined {
return this.users.find(user => user.email === email);
}
getAllUsers(): I_User[] {
return this.users;
}
}
const userRepository = new UserRepository();
userRepository.save({
id: "1",
name: "John Doe",
email: "john@mail.com"
});
userRepository.save({
id: "2",
name: "Alice Brown",
email: "alice@mail.com"
});
// Benutzer suchen
const userOne = userRepository.findById("1");
console.log(userOne);
// Benutzer löschen
const isUserDeleted = userRepository.delete("2");
console.log(isUserDeleted);
// Nicht existierenden Benutzer suchen
const userThree = userRepository.findById("3");
console.log(userThree);
{ id: '1', name: 'John Doe', email: 'john@mail.com' }
true
undefined
Das Interface I_Repository<T>
arbeitet mit einem Platzharlter T
. Die Klasse UserRepository
implementiert das Interface für den konkreten Typ I_User
.
Declaration Merging
TypeScript erlaubt es, mehrere Interface-Definitionen mit demselben Namen zu verschmelzen. Die Properties werden zusammengeführt.
interface I_User {
name: string;
}
interface I_User {
age: number;
}
const user: I_User = {
name: "John",
age: 30
};
Beide Interface-Definitionen werden zu einem einzigen Interface zusammengeführt. Das ist nützlich für Erweiterungen in verschiedenen Modulen oder Bibliotheken (z.B. globale Typen, Frameworks). Sprich, wenn bereits einige Definitionen vorhanden sind und man diese mit eigenen ergänzen möchte.