Was ist TypeScript?
TypeScript ist ein statically typed Superset von JavaScript, das optionale Typ-Annotationen hinzufügt, die zur Kompilierzeit geprüft werden. Es wird in reines JavaScript kompiliert und läuft daher überall dort, wo JavaScript läuft – in Browsern, Node.js, Deno und Bun.
TypeScript wurde 2012 von Microsoft entwickelt und ist mittlerweile die Standardwahl für professionelle JavaScript-Projekte. Der Grund ist simpel: Es findet Bugs, bevor Ihr Code überhaupt ausgeführt wird. Ein Tippfehler in einem Eigenschaftsnamen, ein fehlendes Argument, ein falscher Rückgabetyp – TypeScript markiert diese Fehler bereits beim Kompilieren, anstatt sie zu Abstürzen in der Production werden zu lassen.
Am besten betrachten Sie TypeScript als JavaScript mit einem Sicherheitsnetz. Sie schreiben dieselbe Sprache, die Sie bereits kennen, können aber die Struktur Ihrer Daten und die Verträge Ihrer Funktionen beschreiben. Der Compiler prüft, ob alles zusammenpasst, und entfernt anschließend die Typen, sodass sauberes JavaScript übrig bleibt.
Warum TypeScript wichtig ist
JavaScript ist dynamisch typisiert. Das bedeutet, dass man jedem beliebigen Funktion jeden beliebigen Wert übergeben kann und Fehler erst dann auftreten, wenn der Code tatsächlich ausgeführt wird. In einem kleinen Skript ist das völlig in Ordnung. In einer großen Anwendung mit Dutzenden von Entwicklern ist dies jedoch ein Rezept für Bugs in der Production.
TypeScript löst dieses Problem, indem es die Typen prüft, bevor der Code ausgeführt wird:
- Bugs zur Compile-Zeit finden — Wenn Sie eine Property falsch schreiben, den falschen Argumenttyp übergeben oder vergessen,
nullzu behandeln, weist TypeScript Sie sofort darauf hin. - Bessere Editor-Experience — Autocomplete funktioniert reibungslos, da der Editor genau weiß, welche Properties ein Objekt besitzt. Refactoring ist sicher, weil der Compiler jede Stelle findet, die aktualisiert werden muss.
- Selbstdokumentierender Code — Funktionssignaturen beschreiben exakt, was sie erwarten und zurückgeben. Die Typen verraten Ihnen, wie der Code zu verwenden ist, ohne dass Sie die Implementierung lesen müssen.
- Sichereres Refactoring — Ändern Sie einen Typ, und der Compiler markiert jede Stelle, an der dadurch etwas kaputtgeht. Kein mühsames Durchsuchen der Codebase nach Runtime-Fehlern mehr.
- Progressive Einführung — Sie können TypeScript schrittweise in ein bestehendes JavaScript-Projekt integrieren, Datei für Datei.
TypeScript verursacht keinen Runtime-Overhead. Die Typen werden während der Kompilierung entfernt, sodass das JavaScript, das an die Nutzer ausgeliefert wird, identisch ist – unabhängig davon, ob es in TypeScript geschrieben wurde oder nicht.
So funktioniert TypeScript
TypeScript-Code befindet sich in .ts-Dateien. Der TypeScript-Compiler (tsc) liest diese Dateien, prüft die Typen und gibt .js-Dateien aus, in denen die Typannotationen entfernt wurden.
// app.ts
function add(a: number, b: number): number {
return a + b;
}
const result = add(1, 2); // result is number
Der Compiler prüft, ob a und b Zahlen sind, ob die Funktion eine Zahl zurückgibt und ob result eine Zahl erhält. Wenn etwas nicht übereinstimmt, erhalten Sie einen Fehler zur Kompilierzeit. Das ausgegebene JavaScript sieht einfach so aus:
function add(a, b) {
return a + b;
}
const result = add(1, 2);
Keine Typen, kein Overhead. Die Sicherheit bestand nur im Build-Schritt.
Typ-Annotationen
Typ-Annotationen beschreiben den Typ einer Variable, eines Parameters oder eines Rückgabewerts:
// primitives
let name: string = "Ada";
let age: number = 36;
let active: boolean = true;
// arrays
let skills: string[] = ["HTML", "CSS"];
let scores: Array<number> = [95, 87, 92];
// function parameters and return
function greet(name: string): string {
return `Hello, ${name}!`;
}
Annotationen sind nicht immer erforderlich. TypeScript leitet Typen aus dem Wert ab (Type Inference):
let count = 0; // inferred as number
const items = ["a"]; // inferred as string[]
Bevorzuge die Typ-Ableitung, wenn der Typ offensichtlich ist. Nutze Annotationen, wenn der Typ nicht eindeutig aus dem Wert hervorgeht, wenn du einen allgemeineren Typ benötigst oder wenn die Standard-Ableitung zu spezifisch ist.
Typinferenz
TypeScript leitet Typen automatisch aus dem Kontext ab:
// from assignment
let x = 5; // number
// from return value
function doubling(n: number) {
return n * 2; // inferred as number
}
// from context
const numbers = [1, 2, 3]; // number[]
const doubled = numbers.map(n => n * 2); // number[]
Inferenz bedeutet, dass du weniger Code schreiben musst, während du die gleiche Sicherheit erhältst. Die meisten TypeScript-Codes verlassen sich stark auf die Inferenz – nutze explizite Typannotationen nur dann, wenn sie für mehr Klarheit sorgen.
Union Types
Ein Union Type gibt an, dass ein Wert einer von mehreren Typen angehören kann:
type Status = "loading" | "success" | "error";
function handleStatus(status: Status) {
if (status === "loading") {
// TypeScript knows status is "loading" here
}
}
let value: string | number;
value = "hello";
value = 42;
Unions eignen sich hervorragend zur Modellierung von realen Daten. API-Antworten können beispielsweise erfolgreich sein oder fehlschlagen. Ein Feld könnte ein String oder null sein. Eine Funktion könnte verschiedene Input-Typen akzeptieren.
Type Narrowing
TypeScript schränkt Union Types innerhalb von Conditionals ein (Type Narrowing):
function process(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is string here
return value.toUpperCase();
}
// TypeScript knows value is number here
return value.toFixed(2);
}
Narrowing funktioniert auch mit:
inOperator — prüft, ob eine Eigenschaft existiertinstanceof— prüft Klasseninstanzen- Gleichheitsprüfungen —
===und!== - Discriminated Unions — prüft eine gemeinsame Tag-Eigenschaft
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;
function area(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
}
}
Discriminated Unions gehören zu den mächtigsten Patterns in TypeScript. Die kind Eigenschaft teilt TypeScript genau mit, mit welcher Variante man es gerade zu tun hat.
Interfaces und Type Aliases
Ein Interface beschreibt die Struktur eines Objekts – seine Eigenschaften, deren Typen und ob diese erforderlich sind. Ein Type Alias gibt jedem beliebigen Typ einen Namen, einschließlich Unions, Tuples, Primitiven und Funktionstypen.
// Interface
interface User {
id: number;
name: string;
email: string;
}
// Type alias with the same shape
type User = {
id: number;
name: string;
email: string;
};
Bei einfachen Objektstrukturen sind beide austauschbar. Die Unterschiede werden deutlich, wenn man Typen kombiniert oder erweitert.
Optionale und Readonly-Eigenschaften
interface User {
id: number;
name: string;
email?: string; // optional — string | undefined
readonly createdAt: Date;
}
const user: User = { id: 1, name: "Ada", createdAt: new Date() };
user.createdAt = new Date(); // Error: cannot assign to a readonly property
Interfaces erweitern
Interfaces erben von anderen Interfaces mittels extends, wobei auch mehrere gleichzeitig erweitert werden können:
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface User extends Timestamped {
id: number;
name: string;
}
Type Aliases für Unions, Tuples und Funktionen
Type Aliases spielen ihre Stärken aus, wenn der Typ kein einfaches Objekt ist:
type ID = string | number;
type Status = "idle" | "loading" | "success" | "error";
type Pair = [string, number];
type Callback = (data: unknown) => void;
Intersection Types
Kombiniere Typen mit &:
type User = { id: number; name: string };
type WithTimestamp = { createdAt: Date };
type TimestampedUser = User & WithTimestamp;
Interface oder Type?
Verwende Interfaces für Objektstrukturen, die erweitert oder von Klassen implementiert werden können. Verwende Type Aliases für Unions, Intersections, Tuples und Funktionstypen. Entscheide dich für eine Konvention bei Objektstrukturen und bleibe konsistent.
Generics
Generics sind Typ-Parameter: Sie ermöglichen es einer Funktion, einem Interface oder einer Klasse, mit jedem beliebigen Typ zu arbeiten, während die Typsicherheit erhalten bleibt. Anstatt einen Typ fest zu codieren oder auf any zurückzugreifen, übergibt man eine Typvariable, die erst beim Aufruf festgelegt wird.
function first<T>(items: T[]): T | undefined {
return items[0];
}
const num = first([1, 2, 3]); // number
const str = first(["a", "b"]); // string
Ohne Generics müsste man entweder any verwenden und damit Typinformationen verlieren oder für jeden Typ eine separate Funktion schreiben.
Generische Funktionen
Ein Typ-Parameter wird in spitzen Klammern deklariert. TypeScript leitet diesen normalerweise aus den Argumenten ab, sodass man ihn selten explizit übergeben muss:
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}
const result = merge({ name: "Ada" }, { age: 36 });
// { name: string; age: number }
Generische Interfaces und Klassen
interface Response<T> {
data: T;
status: number;
}
interface User {
id: number;
name: string;
}
const response: Response<User> = {
data: { id: 1, name: "Ada" },
status: 200,
};
class Stack<T> {
private items: T[] = [];
push(item: T) { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
Constraints
Verwenden Sie extends, um einzuschränken, welche Typen ein Generic akzeptieren kann:
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
Standard-Typ-Parameter
interface Paginated<T = unknown> {
data: T[];
total: number;
page: number;
}
const response: Paginated = { data: [1, "a"], total: 10, page: 1 };
Generics werden zur Laufzeit entfernt (Type Erasure), sodass sie keinen Overhead verursachen – sie dienen lediglich dazu, den Compiler und Ihren Editor intelligenter zu machen.
Die tsconfig.json
Die Datei tsconfig.json konfiguriert den TypeScript-Compiler:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Wichtige Optionen:
- target — welche JavaScript-Version ausgegeben werden soll (ES2020 ist ein guter Standardwert)
- strict — aktiviert alle Flags für die strikte Typprüfung
- module — das Modulsystem für die Ausgabe (ESNext für moderne Projekte)
- outDir — das Zielverzeichnis für das kompilierte JavaScript
- esModuleInterop — ermöglicht Default-Imports aus CommonJS-Modulen
Verwenden Sie für neue Projekte immer "strict": true. Dadurch werden mehr Bugs abgefangen, auch wenn der Code dadurch etwas expliziter geschrieben werden muss.
TypeScript installieren
# As a project dependency (recommended)
npm install --save-dev typescript
# Globally (not recommended for teams)
npm install -g typescript
Kompiliere dein Projekt:
# Using npx
npx tsc
# Or add a script to package.json
npm run build
Für die Entwicklung im Watch-Modus:
npx tsc --watch
Die meisten modernen Frameworks (Next.js, Vite, Astro) übernehmen die TypeScript-Kompilierung automatisch für dich – du musst tsc nicht manuell ausführen.
Häufige Fehler
anyüberall zu verwenden, was die Typprüfung deaktiviert.- Alles zu annotieren, obwohl die Typinferenz den Typ bereits kennt.
- Den Strict Mode in der tsconfig nicht zu aktivieren.
- TypeScript-Typen mit Runtime-Werten zu verwechseln – Typen werden zur Compile-Zeit entfernt.
- Typen falsch zu importieren – verwenden Sie
import typefür reine Typ-Imports. - Compiler-Fehler zu ignorieren, anstatt sie zu beheben.
- Keine
@types-Pakete für JavaScript-Bibliotheken zu installieren. - Jede Eigenschaft eines Interfaces optional zu machen, wodurch notwendige Prüfungen verborgen bleiben.
anyanstelle eines Generics zu verwenden, wodurch die Typinformationen verloren gehen, die man eigentlich behalten wollte.
Best Practices
- Aktiviere den Strict Mode vom ersten Tag an.
- Überlass die Arbeit der Typinferenz – nutze explizite Annotationen nur dort, wo sie einen Mehrwert bieten.
- Verwende
unknownanstelle vonany, wenn der Typ unbekannt ist. - Bevorzuge Interfaces für Objektstrukturen, die möglicherweise erweitert werden.
- Nutze Discriminated Unions für State Machines und variantenbasierte Daten.
- Halte Typdefinitionen nah an der Stelle, an der sie verwendet werden.
- Nutze
import typefür reine Typ-Imports, um den Runtime-Code schlank zu halten. - Betrachte den Compiler als Teammitglied, nicht als Hindernis.
Was du als Nächstes lernen solltest
Du beherrschst nun die TypeScript-Grundlagen: Annotations, Inference, Unions, Narrowing, Interfaces, Type Aliases und Generics. Der nächste logische Schritt sind Advanced Types, um Utility Types, Conditional Types und Mapped Types kennenzulernen. Such dir ein Projekt aus, refactore etwas Code und vertiefe dein Wissen durch die Praxis.