Erste Schritte
Hello World & Kompilierung
TypeScript-Dateien verwenden die .ts-Erweiterung. Der tsc-Compiler transpiliert TS zu JS und löscht alle Typ-Annotationen zur Runtime. Verwende --strict für maximale Typsicherheit. ts-node oder bun kann .ts-Dateien direkt ausführen ohne separaten Kompilierungsschritt.
// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);
// Compile to JavaScript (type annotations erased):
// tsc hello.ts -> hello.js
// tsc --strict hello.ts // enable all strict checks
// tsc --watch hello.ts // recompile on change
// Run directly with ts-node or bun:
// ts-node hello.tstsconfig.json
tsconfig.json konfiguriert den TypeScript-Compiler. 'strict: true' aktiviert noImplicitAny, strictNullChecks, strictFunctionTypes und mehr. 'target' kontrolliert die Ausgabe-JS-Version. 'esModuleInterop' ermöglicht Default-Imports aus CommonJS-Modulen wie Nodes Built-ins.
{
"compilerOptions": {
"target": "ES2020", // JS version to emit
"module": "ESNext", // module system
"strict": true, // enable all strict checks
"outDir": "./dist", // output directory
"rootDir": "./src", // source root
"esModuleInterop": true, // allow default imports from CJS
"skipLibCheck": true, // skip .d.ts type checking
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Typ-Annotationen & Inferenz
Typ-Annotationen geben den Typ einer Variablen explizit an. TypeScript kann Typen auch aus Werten ableiten. Verwende explizite Annotationen für Funktionssignaturen und öffentliche APIs; verlasse dich auf Inferenz für lokale Variablen. Vermeide 'any' – es optet komplett aus der Typprüfung aus.
// Explicit type annotations
let count: number = 0;
const name: string = "Alice";
let isDone: boolean = true;
let ids: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 30];
// Type inference (let TS figure it out)
let age = 30; // inferred as number
let items = [1, 2, 3]; // inferred as number[]
let mixed = [1, "a"]; // inferred as (string | number)[]
// any disables type checking (avoid!)
let anything: any = 42;
anything = "string"; // no errorStrict-Mode-Prüfungen
Strict Mode aktiviert kritische Prüfungen: strictNullChecks (null/undefined nicht anderen Typen zuweisbar), noImplicitAny (Parameter müssen Typen haben), strictPropertyInitialization (Klassen-Felder müssen initialisiert werden). Verwende '!' (Definite Assignment), wenn du sicher bist, dass ein Feld später gesetzt wird.
// strictNullChecks: null/undefined not assignable to other types
let n: string = null; // Error in strict mode
// noImplicitAny: must annotate parameters
function greet(name) { } // Error: implicit any
// strictPropertyInitialization
class User {
name: string; // Error: not initialized
constructor() {}
}
// Fix: initialize or use definite assignment
class Fixed {
name!: string; // definite assignment assertion
age: number = 0;
}Deklarationsdateien (.d.ts)
Deklarationsdateien (.d.ts) bieten Typen für JavaScript-Bibliotheken ohne TypeScript-Definitionen. 'declare' teilt dem Compiler mit, dass eine Variable/Funktion zur Runtime existiert. Verwende @types-Pakete von DefinitelyTyped für beliebte Bibliotheken (z.B. @types/node, @types/react).
// types.d.ts - type declarations only, no implementation
declare module "my-lib" {
export function greet(name: string): string;
export const version: string;
}
// global.d.ts - extend global scope
declare global {
interface Window {
myApp: { version: string };
}
}
// Usage in .ts files:
// import { greet } from "my-lib"; // now typed
// Install @types packages: npm i -D @types/node @types/reactGrundtypen
Primitive & spezielle Typen
TypeScript-Primitive: string, number, boolean, bigint, symbol. 'void' gibt an, dass eine Funktion keinen Wert zurückgibt. 'never' repräsentiert Werte, die nie auftreten – Funktionen, die werfen oder für immer laufen. Verwende 'never' für erschöpfende Prüfungen in switch-Anweisungen.
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("id");
// void: function returns nothing
function log(msg: string): void { console.log(msg); }
// never: function never returns
function fail(msg: string): never { throw new Error(msg); }
function infinite(): never { while (true) {} }Arrays & Tuples
Arrays verwenden entweder T[] oder Array<T>-Syntax. ReadonlyArray verhindert Mutationen. Tuples sind Arrays fester Länge mit spezifischen Typen an jedem Index – nützlich für Schlüssel-Wert-Paare oder CSV-ähnliche Daten. Beschriftete Tuples verbessern die Lesbarkeit mit benannten Positionen.
// Two syntaxes for arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];
// ReadonlyArray (immutable)
const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // Error: not mutable
// Tuple (fixed length, known types)
let tuple: [string, number] = ["Alice", 30];
let name = tuple[0]; // string
let age = tuple[1]; // number
// Labeled tuple elements (TS 4.0+)
let entry: [name: string, age: number] = ["Bob", 25];Enums
Enums definieren eine Menge benannter Konstanten. String-Enums werden für Debugging empfohlen (Werte sind in der Ausgabe lesbar). Numerische Enums unterstützen Reverse Mapping. 'const enum' wird zur Compile-Zeit gelöscht (inlined) für zero Runtime-Cost. Bevorzuge Union-Typen für einfache Fälle.
// Numeric enum (default starts at 0)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up; // 0
// String enum (recommended for readability)
enum Status {
Pending = "PENDING",
Success = "SUCCESS",
Failed = "FAILED",
}
// Reverse mapping (numeric only)
console.log(Direction[0]); // "Up"
// Const enum (inlined, no runtime object)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;any vs unknown vs never
'any' deaktiviert alle Typprüfungen – vermeide es. 'unknown' ist die typsichere Alternative: Du musst es vor der Verwendung eingrenzen (via typeof, instanceof). 'never' repräsentiert Werte, die nie auftreten, verwendet für Erschöpfungsprüfung in switch-Anweisungen, um fehlende Cases zur Compile-Zeit zu erkennen.
// any: opt out of type checking (dangerous!)
let a: any = 42;
a = "string"; // OK
a.toUpperCase(); // OK (no check, may fail at runtime)
// unknown: type-safe alternative to any
let u: unknown = 42;
// u.toUpperCase(); // Error: unknown type
if (typeof u === "string") {
u.toUpperCase(); // OK after narrowing
}
// never: impossible value (exhaustiveness check)
type Shape = "circle" | "square";
function area(s: Shape) {
switch (s) {
case "circle": return Math.PI;
case "square": return 1;
default:
const _exhaustive: never = s; // Error if case missing
}
}Type-Assertions
Type-Assertions sagen dem Compiler 'vertrau mir, ich kenne den Typ'. Verwende 'as'-Syntax. Die Non-Null-Assertion (!) sagt TS, dass ein Wert nicht null/undefined ist. 'as const' macht alle Eigenschaften zu Readonly-Literalen – nützlich für Config-Objekte und Redux-Action-Typen. Assertions ändern das Runtime-Verhalten nicht.
// as syntax (preferred, works in .tsx)
let val: unknown = "hello";
let len: number = (val as string).length;
// Non-null assertion (!)
let el = document.querySelector("#app")!;
el.innerHTML = "Hi"; // el is HTMLElement, not null
// Double assertion (for unsafe casts)
let value = "42" as unknown as number;
// const assertion (literal types)
const req = { method: "GET", url: "/api" } as const;
// req.method has type "GET" (not string)
// req is readonlyLiteral- & Union-Typen
Literal-Typen beschränken einen Wert auf einen spezifischen String, Number oder Boolean. Kombiniert mit Unions erstellen sie präzise Typen wie direction oder HTTP-Methoden. Template-Literal-Typen (TS 4.1+) bauen String-Typen aus anderen Typen – mächtig für das Generieren typsicherer Schlüssel und Event-Namen.
// String literal types
let direction: "left" | "right" | "up" | "down";
direction = "left"; // OK
// direction = "sideways"; // Error
// Numeric literal types
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 4; // OK
// Boolean literal
let flag: true = true;
// Template literal types (TS 4.1+)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"Interfaces & Objekte
Interface-Grundlagen
Interfaces beschreiben die Form von Objekten. '?' markiert optionale Eigenschaften (können undefined sein). 'readonly' verhindert Neuzuweisung nach Initialisierung. Interfaces sind nur Compile-Zeit – sie werden im Ausgabe-JavaScript gelöscht. Verwende sie, um Verträge für Objekte und Klassen zu definieren.
interface User {
id: number;
name: string;
email?: string; // optional property
readonly createdAt: Date; // immutable
}
const user: User = {
id: 1,
name: "Alice",
createdAt: new Date(),
};
// user.createdAt = new Date(); // Error: readonly
// user.email; // string | undefinedIndex-Signaturen
Index-Signaturen erlauben Objekte mit beliebigen Schlüsseln eines gegebenen Typs. Alle Eigenschaftswerte müssen dem Index-Typ zuweisbar sein. Nützlich für Dictionaries, Caches und dynamische Daten. Kombiniere mit bekannten Eigenschaften für typisierte Configs mit zusätzlichen Optionen.
// Index signature: arbitrary string keys
interface StringMap {
[key: string]: string;
}
const dict: StringMap = {
name: "Alice",
city: "NYC",
// count: 42, // Error: value must be string
};
// Mixed: known + index signature
interface Config {
name: string;
[key: string]: string | number;
}
// Readonly index signature
interface ReadonlyMap {
readonly [key: string]: number;
}Interfaces erweitern
Interfaces können ein oder mehrere andere Interfaces erweitern und deren Member kombinieren. Das ermöglicht Komposition und Code-Wiederverwendung. Anders als Klassen unterstützen Interfaces Mehrfachvererbung. Wenn du ein Interface implementierst, muss die Klasse alle erforderlichen Member bereitstellen.
interface Animal {
name: string;
eat(): void;
}
interface Pet extends Animal {
owner: string;
play(): void;
}
interface Swimmer {
swim(): void;
}
// Multiple inheritance
interface Duck extends Pet, Swimmer {
quack(): void;
}
const duck: Duck = {
name: "Donald",
owner: "Walt",
eat() {},
play() {},
swim() {},
quack() {},
};Funktionstypen in Interfaces
Interfaces können Funktionssignaturen beschreiben und typsichere Callbacks ermöglichen. Hybride Interfaces (callable + Eigenschaften) werden für jQuery-artige Funktionen verwendet, die auch Methoden haben. Dieses Pattern ist in Bibliotheken verbreitet, die Funktionen mit angehängten Helfern zurückgeben.
interface SearchFn {
(source: string, keyword: string): boolean;
}
const contains: SearchFn = (src, kw) => src.includes(kw);
console.log(contains("hello world", "world")); // true
// Interface with mixed members (hybrid)
interface Counter {
(start: number): void; // callable
count: number; // property
reset(): void; // method
}
// Function with properties (jQuery-style)
const counter: any = (n: number) => { counter.count = n; };
counter.count = 0;
counter.reset = () => { counter.count = 0; };Interface vs Type Alias
Interfaces unterstützen Declaration Merging (gleichnamige Interfaces kombinieren), bessere Fehlermeldungen und werden für Objekt-/Klassen-Formen bevorzugt. Type-Aliase sind flexibler (können Unions, Primitive, Tuples repräsentieren), können aber nicht gemerged werden. Verwende Interfaces für erweiterbare APIs, Type-Aliase für Unions und berechnete Typen.
// Interface: extendable, better error messages
interface Window { title: string; }
interface Window { size: number; } // declaration merging
const w: Window = { title: "App", size: 800 };
// Type alias: more flexible (unions, primitives, etc.)
type ID = string | number;
type Callback<T> = (value: T) => void;
// Both can describe object shapes
interface UserI { name: string; }
type UserT = { name: string; };
// Use interface for objects/classes, type for unions/aliasesOptional Chaining & Nullish Coalescing
Optional Chaining (?.) greift sicher auf verschachtelte Eigenschaften zu – gibt undefined zurück statt zu werfen, wenn ein Glied null/undefined ist. Nullish Coalescing (??) bietet einen Default nur für null/undefined (nicht 0 oder ''). Diese Operatoren reduzieren ausführlichen Null-Prüf-Code drastisch.
interface User {
profile?: {
address?: {
city?: string;
};
};
}
const user: User = {};
// Optional chaining (?.) - safe property access
const city = user.profile?.address?.city; // string | undefined
// Nullish coalescing (??) - default value
const name = user.profile?.address?.city ?? "Unknown";
// Non-null assertion (!) - you're sure it's not null
// const c = user.profile!.address!.city!; // risky
// Optional method call
const result = user.profile?.address?.city?.toUpperCase();Type-Aliase & Unions
Type-Aliase
Type-Aliase erstellen benannte Referenzen auf jeden Typ, einschließlich Unions, Intersections, Primitive und Generics. Anders als Interfaces können Aliase nicht gemerged oder erweitert werden, aber sie sind flexibler. Verwende Aliase für Unions, Tuples und Utility-Typen; verwende Interfaces für Objekt-Formen.
// Basic alias
type ID = string | number;
type Point = { x: number; y: number };
// Generic alias
type Container<T> = { value: T };
// Function type alias
type Handler<T> = (event: T) => void;
// Usage
const id: ID = 42;
const p: Point = { x: 1, y: 2 };
const box: Container<string> = { value: "hi" };
const onClick: Handler<string> = (e) => console.log(e);Union-Typen
Union-Typen (A | B) erlauben einem Wert, einer von mehreren Typen zu sein. TypeScript grenzt den Typ innerhalb bedingter Blöcke mit typeof, instanceof oder in-Prüfungen ein. Hinweis: (string | number)[] ist unterschiedlich von string[] | number[] – ersteres ist ein gemischtes Array, letzteres ist all-strings ODER all-numbers.
// Union: value can be one of several types
type ID = string | number;
function display(id: ID) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // narrowed to string
} else {
console.log(id.toFixed(2)); // narrowed to number
}
}
display("abc"); // ABC
display(42); // 42.00
// Union of arrays vs array of unions
type Mixed = (string | number)[];
type Either = string[] | number[];Intersection-Typen
Intersection-Typen (A & B) kombinieren alle Member mehrerer Typen – das Ergebnis muss jedem Typ entsprechen. Nützlich für Mixins, Komposition und das Zusammenführen von Utility-Typen. Anders als Union (ODER) ist Intersection UND: Der Wert muss alle Eigenschaften aller Typen haben.
// Intersection: combine multiple types into one
interface BusinessPartner {
name: string;
credit: number;
}
interface Identity {
id: number;
email: string;
}
type Employee = BusinessPartner & Identity;
const emp: Employee = {
name: "Alice",
credit: 1000,
id: 1,
email: "[email protected]",
};
// All properties required
// const bad: Employee = { name: "Bob" }; // Error: missing propsNullable-Typen
Im Strict Mode sind null und undefined anderen Typen nicht zuweisbar – du musst sie explizit mit Unions einschließen (string | null). Optionale Parameter (param?) sind implizit T | undefined. Verwende ?? für sichere Defaults und ! für Non-Null-Assertion (sparsam verwenden).
// In strict mode, null/undefined are separate types
let name: string | null = null;
name = "Alice"; // OK
// Optional parameters are implicitly | undefined
function greet(name?: string) {
// name is string | undefined
console.log(name ?? "Guest");
}
// Return type can be null
function find(id: number): string | null {
return id === 1 ? "Alice" : null;
}
// Non-null assertion
const result = find(1)!.toUpperCase(); // "ALICE"Keyof- & Typeof-Operatoren
'keyof T' extrahiert die Schlüssel des Typs T als String-Literal-Union. 'typeof x' extrahiert den Typ eines Wertes (nützlich für das Ableiten aus Objekten). 'keyof typeof obj' kombiniert beide, um die Schlüssel eines existierenden Objekts zu erhalten – häufig in Redux-Action-Typen und typsicheren Property-Accessors.
interface User {
id: number;
name: string;
email: string;
}
// keyof: extract keys as a union
type UserKey = keyof User; // "id" | "name" | "email"
function getProp(obj: User, key: keyof User) {
return obj[key];
}
// typeof: extract type from a value
const config = { port: 3000, host: "localhost" };
type Config = typeof config; // { port: number; host: string }
// keyof typeof: keys of an object
type ConfigKey = keyof typeof config; // "port" | "host"Mapped Types
Mapped Types iterieren über Schlüssel, um einen Typ zu transformieren. Eingebaute Utilities wie Readonly, Partial und Pick sind Mapped Types. Verwende + und - Modifikatoren, um readonly oder optional hinzuzufügen/zu entfernen. Key-Remapping (TS 4.1+) benennt Schlüssel mit Template-Literal-Typen um – mächtig für das Generieren von Getter-/Setter-Typen.
// Map over keys to create a new type
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
interface User { id: number; name: string; }
type ReadonlyUser = Readonly<User>; // all readonly
type OptionalUser = Optional<User>; // all optional
// Modifiers: +add, -remove
type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };
// Key remapping (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};Funktionen
Funktionstypen & Signaturen
TypeScript fügt Funktionsparametern und Rückgabewerten Typ-Annotationen hinzu. Der Rückgabetyp kann oft abgeleitet werden, aber explizite Annotation wird für öffentliche APIs empfohlen. Default-Parameter machen Argumente optional mit einem Fallback-Wert. Verwende void, wenn eine Funktion nichts zurückgibt.
// Named function with types
function add(a: number, b: number): number {
return a + b;
}
// Arrow function with types
const multiply = (a: number, b: number): number => a * b;
// Function type alias
type MathOp = (a: number, b: number) => number;
const divide: MathOp = (a, b) => a / b;
// Void return (no return value)
function log(msg: string): void { console.log(msg); }
// Optional and default parameters
function greet(name: string, greeting: string = "Hi"): string {
return `${greeting}, ${name}!`;
}
greet("Alice"); // "Hi, Alice!"
greet("Bob", "Hello"); // "Hello, Bob!"Rest-Parameter & Tuples
Rest-Parameter (...args) sammeln mehrere Argumente in einem Array. TypeScript typisiert sie als T[] oder als Tuple für variadische Funktionen fester Länge. Der Spread-Operator (...) macht das Gegenteil – expandiert ein Array in einzelne Argumente. Tuple-Rest-Typen ermöglichen präzise variadische Signaturen.
// Rest parameters (variadic)
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// Tuple rest (fixed prefix + variadic tail)
function pair(name: string, ...scores: number[]): void {
console.log(name, scores);
}
// Spread call
const nums = [1, 2, 3];
console.log(sum(...nums));
// Typed rest as tuple
function f(...args: [string, number, boolean]): void {
const [s, n, b] = args;
}Funktions-Overloads
Funktions-Overloads bieten mehrere Typ-Signaturen für dieselbe Funktion und ermöglichen präzise Rückgabetypen basierend auf der Eingabe. Die Implementierungs-Signatur ist vor Aufrufern verborgen. Overloads werden Top-Down aufgelöst – setze spezifischere Signaturen zuerst. Häufig in Bibliotheken wie jQuery und lodash.
// Overload signatures (what callers see)
function parse(input: string): string[];
function parse(input: number): number[];
// Implementation signature (not visible to callers)
function parse(input: string | number): string[] | number[] {
if (typeof input === "string") {
return input.split(",");
}
return [input, input * 2];
}
const strs = parse("a,b,c"); // string[]
const nums = parse(42); // number[]
// Overloads with different param counts
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTs: number, m?: number, d?: number): Date {
return m === undefined
? new Date(yOrTs)
: new Date(yOrTs, m - 1, d);
}this-Typ
TypeScript lässt dich den 'this'-Typ als ersten Parameter deklarieren. Das stellt sicher, dass die Funktion mit dem korrekten Kontext aufgerufen wird – nützlich für Methoden, die als Callbacks übergeben werden. Arrow-Funktionen erfassen 'this' lexikalisch und vermeiden die Notwendigkeit von .bind(). Verwende 'noImplicitThis', um untypisierte 'this'-Errors abzufangen.
interface Card {
suit: string;
rank: string;
isFaceUp(): boolean;
}
// Explicit 'this' parameter
function format(this: Card): string {
return `${this.rank} of ${this.suit}`;
}
const card: Card = {
suit: "Hearts",
rank: "A",
isFaceUp() { return true; },
format,
};
// 'this' in callbacks with bind
class Handler {
private count = 0;
increment = () => { this.count++; }; // arrow binds this
}Callbacks & Higher-Order-Funktionen
TypeScript typisiert Higher-Order-Funktionen (Funktionen, die Funktionen nehmen oder zurückgeben) vollständig. Verwende generische Typparameter (T, U), um Typbeziehungen zwischen Eingabe und Ausgabe zu bewahren. Callback-Typen werden häufig als Type-Aliase für Wiederverwendung definiert. Currying (Funktionen zurückgeben) ist vollständig typsicher.
// Function as parameter
type Callback<T> = (value: T, index: number) => void;
function forEach<T>(arr: T[], cb: Callback<T>): void {
for (let i = 0; i < arr.length; i++) {
cb(arr[i], i);
}
}
forEach(["a", "b"], (v, i) => console.log(i, v));
// Function returning function (curry)
function add(a: number): (b: number) => number {
return (b) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Generic map
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
return arr.map(fn);
}Parameter-Destructuring
TypeScript unterstützt Destructuring in Funktionsparametern – annotiere die destrukturierte Form inline oder über ein Interface. Das Extrahieren in ein Interface verbessert Lesbarkeit und Wiederverwendung. Array-/Tuple-Destructuring funktioniert auch. Dieses Pattern ist häufig in React-Component-Props und API-Handlern.
// Destructured parameters with types
function createUser({ name, age, email }: {
name: string;
age: number;
email?: string;
}): void {
console.log(name, age, email);
}
createUser({ name: "Alice", age: 30 });
// Extract to interface for reuse
interface UserOpts {
name: string;
age: number;
email?: string;
}
function updateUser({ name, age }: UserOpts): void {}
// Array destructuring in params
function swap([a, b]: [number, number]): [number, number] {
return [b, a];
}Klassen & OOP
Klasse & Konstruktor
TypeScript-Klassen unterstützen Parameter-Properties – das Präfixieren von Konstruktor-Params mit Access-Modifiern (public/private/protected/readonly) erstellt und weist Felder automatisch zu. Diese Kurzschreibweise reduziert Boilerplate. Methoden können Typ-Annotationen bei Rückgabewerten haben. Felder sind standardmäßig public.
class Person {
// Parameter properties (shorthand)
constructor(
public name: string, // auto-creates this.name
private age: number, // private field
readonly id: number, // immutable
) {}
greet(): string {
return `Hi, I'm ${this.name}`;
}
}
const p = new Person("Alice", 30, 1);
console.log(p.name); // "Alice" (public)
// p.age; // Error: private
// p.id = 2; // Error: readonlyAccess-Modifier
Access-Modifier: public (Standard, überall), private (nur Klasse), protected (Klasse + Subklassen), readonly (unveränderlich). TypeScripts 'private' ist nur Compile-Zeit; ES '#' private Felder sind zur Runtime wirklich privat. Verwende private für Implementierungsdetails, protected für Erweiterungspunkte.
class BankAccount {
public owner: string; // accessible everywhere
private balance: number; // class only
protected rate: number; // class + subclasses
readonly id: string; // immutable after init
#secret: string; // ES private (runtime)
constructor(owner: string) {
this.owner = owner;
this.balance = 0;
this.rate = 0.05;
this.id = crypto.randomUUID();
this.#secret = "hidden";
}
deposit(amount: number): void {
this.balance += amount;
}
}Vererbung & abstrakte Klassen
Abstrakte Klassen können nicht direkt instanziiert werden – sie definieren eine Basis für Subklassen. Abstrakte Methoden haben keine Implementierung in der Basisklasse; Subklassen müssen sie implementieren. Verwende 'extends' für Vererbung und 'super()' für den Aufruf des Eltern-Konstruktors. Abstrakte Klassen ermöglichen Polymorphismus – Code kann mit jeder Shape-Subklasse arbeiten.
abstract class Shape {
constructor(public color: string) {}
abstract area(): number; // must be implemented
describe(): string {
return `${this.color} shape, area ${this.area()}`;
}
}
class Circle extends Shape {
constructor(color: string, private r: number) {
super(color);
}
area(): number { return Math.PI * this.r ** 2; }
}
class Square extends Shape {
constructor(color: string, private side: number) {
super(color);
}
area(): number { return this.side ** 2; }
}
const c = new Circle("red", 5);
console.log(c.describe()); // "red shape, area 78.54..."
// new Shape("blue"); // Error: cannot instantiate abstractInterfaces & Implements
Eine Klasse kann mehrere Interfaces implementieren (durch Kommas getrennt). Die Klasse muss alle Interface-Member bereitstellen. Anders als extends (Einfachvererbung) unterstützt implements mehrere Verträge. Das ist TypeScripts Weg, um Mehrfachvererbungs-ähnliches Verhalten zu erreichen. Verwende Interfaces, um Verträge zu definieren, Klassen, um sie zu implementieren.
interface Printable {
toString(): string;
}
interface Comparable<T> {
compareTo(other: T): number;
}
class Money implements Printable, Comparable<Money> {
constructor(private amount: number) {}
toString(): string {
return `$${this.amount.toFixed(2)}`;
}
compareTo(other: Money): number {
return this.amount - other.amount;
}
}
const a = new Money(10);
const b = new Money(20);
console.log(a.toString()); // "$10.00"
console.log(a.compareTo(b)); // -10Getter & Setter
Getter und Setter fangen Eigenschaftszugriffe ab für Validierung, Berechnung oder Side-Effects. Verwende ein privates Backing-Field (Konvention: Unterstrich-Präfix). Getter ermöglichen berechnete Eigenschaften (wie fahrenheit aus celsius). Setter ermöglichen Validierung. Greife wie reguläre Eigenschaften zu – keine Klammern.
class Temperature {
private _celsius: number = 0;
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) throw new Error("Below absolute zero");
this._celsius = value;
}
get fahrenheit(): number {
return this._celsius * 9 / 5 + 32;
}
set fahrenheit(value: number) {
this.celsius = (value - 32) * 5 / 9;
}
}
const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // throwsStatische Member & Singletons
Statische Member gehören zur Klasse, nicht zu Instanzen – Zugriff über ClassName.member. Verwende static für Konstanten, Utility-Funktionen und Factory-Methoden. Ein privater Konstruktor + static getInstance() implementiert das Singleton-Pattern. 'as const' macht statische Arrays readonly mit Literal-Typen.
class Logger {
static instance: Logger;
private logs: string[] = [];
private constructor() {} // prevent direct new
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
static readonly LEVELS = ["INFO", "WARN", "ERROR"] as const;
log(msg: string): void {
this.logs.push(msg);
}
}
const logger = Logger.getInstance();
console.log(Logger.LEVELS); // ["INFO", "WARN", "ERROR"]
// new Logger(); // Error: private constructorGenerics
Generische Funktionen
Generics (<T>) lassen dich Funktionen schreiben, die mit jedem Typ arbeiten, während die Typsicherheit bewahrt wird. Der Typparameter T ist ein Platzhalter, der zur Aufrufzeit ausgefüllt wird – entweder explizit (identity<number>) oder aus Argumenten abgeleitet. Generics ermöglichen wiederverwendbare, typsichere Datenstrukturen und Algorithmen.
// Generic function: preserves type relationship
function identity<T>(value: T): T {
return value;
}
const n = identity<number>(42); // T = number, returns number
const s = identity("hi"); // T inferred as string
// Generic with multiple type params
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("Alice", 30); // [string, number]
// Generic with array
function first<T>(arr: T[]): T | undefined {
return arr[0];
}Generische Klassen
Generische Klassen (<T>) erstellen typsichere Container. Jede Instanz sperrt einen spezifischen Typ ein – ein Stack<number> akzeptiert nur Zahlen. Das fängt Typfehler zur Compile-Zeit ab ohne Runtime-Overhead (Generics werden gelöscht). Häufig in Collections (Stack, Queue, Map) und reaktiven Wrappern (Observable<T>).
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2
const strStack = new Stack<string>();
strStack.push("hello");Generische Constraints
Constraints (T extends SomeType) beschränken, welche Typen ein Generic akzeptieren kann. 'T extends HasLength' stellt sicher, dass T eine 'length'-Eigenschaft hat. 'K extends keyof T' (keyof-Constraint) stellt sicher, dass ein Schlüssel auf einem Objekt existiert, und gibt den korrekten Werttyp zurück. Constraints ermöglichen typsichere Eigenschaftszugriffe und Methodenaufrufe auf Generics.
// Constraint: T must have a 'length' property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength("hello"); // 5 (string has length)
logLength([1, 2, 3]); // 3 (array has length)
// logLength(42); // Error: number has no length
// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
// getProperty(user, "email"); // Error: not a keyDefault-Typparameter
Default-Typparameter bieten einen Fallback-Typ, wenn keiner angegeben ist. Nützlich für APIs mit einem häufigen Fall (z.B. ApiResponse defaultet zu string). Defaults können von früheren Parametern abhängen. Kombiniere mit Constraints (T extends X = DefaultType) für typsichere optionale Generics.
// Default type parameter
interface ApiResponse<T = string> {
data: T;
status: number;
}
const r1: ApiResponse = { data: "hello", status: 200 }; // T = string
const r2: ApiResponse<number> = { data: 42, status: 200 }; // T = number
// Multiple defaults
class Container<T = string, U = number> {
constructor(public a: T, public b: U) {}
}
const c1 = new Container("hi", 42); // Container<string, number>
const c2 = new Container<boolean>(true); // Container<boolean, number>
// Default with constraint
interface Box<T extends object = { id: number }> {
value: T;
}Generische Interfaces & Typen
Generische Interfaces und Type-Aliase erstellen wiederverwendbare, typsichere Verträge. Repository<T> abstrahiert Datenzugriff mit einer konsistenten API. Result<T, E> ist eine discriminated union für Fehlerbehandlung ohne Exceptions. Default-Typparameter (E = Error) reduzieren Boilerplate für häufige Fälle.
// Generic interface
interface Repository<T> {
findById(id: string): Promise<T>;
save(item: T): Promise<void>;
delete(id: string): Promise<void>;
}
// Implement with concrete type
class UserRepo implements Repository<User> {
async findById(id: string): Promise<User> { /* ... */ }
async save(user: User): Promise<void> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
}
// Generic type alias with conditional
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
const success: Result<number> = { ok: true, value: 42 };
const failure: Result<string> = { ok: false, error: "not found" };Conditional Types
Conditional Types (T extends U ? X : Y) sind If-Anweisungen auf Typebene. 'infer R' extrahiert einen Typ aus einem anderen Typ (z.B. den Rückgabetyp einer Funktion). Conditional Types verteilen sich über Unions – ToArray<string | number> wird zu string[] | number[]. Eingebaute Utilities wie Exclude, Extract und NonNullable verwenden dies.
// Conditional type: if T extends U, use X, else Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>; // false
// Extract return type
type AsyncReturnType<T> =
T extends (...args: any[]) => Promise<infer R> ? R : never;
async function fetchUser(): Promise<User> { /* ... */ }
type U = AsyncReturnType<typeof fetchUser>; // User
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
// Exclude / Extract built on conditionals
type NonNullable<T> = T extends null | undefined ? never : T;Erweiterte Typen
Utility-Typen
TypeScript bietet eingebaute Utility-Typen für häufige Transformationen: Partial (alle optional), Pick (Schlüssel auswählen), Omit (Schlüssel ausschließen), Record (Schlüssel-Wert-Map), Required (optional entfernen), ReturnType (Funktionsrückgabe), Parameters (Funktions-Params als Tuple). Diese eliminieren repetitive Typdefinitionen.
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial: all optional (for updates)
type UserUpdate = Partial<User>;
const patch: UserUpdate = { name: "Bob" };
// Pick: select specific keys
type UserSummary = Pick<User, "id" | "name">;
// Omit: exclude specific keys
type CreateUser = Omit<User, "id">;
// Record: key-value map
type UserMap = Record<string, User>;
// Required: all required (remove ?)
type StrictUser = Required<Partial<User>>;
// ReturnType: function return type
type R = ReturnType<() => string>; // string
// Parameters: function parameter types as tuple
type P = Parameters<(a: number, b: string) => void>; // [number, string]Template-Literal-Typen
Template-Literal-Typen (TS 4.1+) bauen String-Typen durch Interpolation anderer Typen. Kombiniert mit Unions generieren sie kartesische Produkte von Strings. Capitalize/Uppercase transformieren die Groß-/Kleinschreibung. Verwende sie für typsichere Event-Namen, Getter-/Setter-Generierung und API-Route-Typisierung. Die 'as'-Klausel in Mapped Types ermöglicht Key-Umbenennung.
// Build string types from other types
type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"foo">; // "Foo"
// Getter names from keys
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
// Event listener types
type EventName = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"infer-Schlüsselwort
Das 'infer'-Schlüsselwort deklariert eine Typvariable innerhalb der extends-Klausel eines Conditional Types und erfasse einen Typ zur Wiederverwendung. Es ist die Grundlage von Utility-Typen wie ReturnType, Parameters und Awaited. Verwende infer, um Typen aus komplexen Strukturen (Arrays, Promises, Funktionen) zu extrahieren, ohne sie manuell zu zerlegen.
// Extract return type of a function
type MyReturnType<T> =
T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => string>; // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean
// Extract element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>; // string
// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type V = Unwrap<Promise<number>>; // number
// Extract first parameter
type FirstParam<T> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>; // stringDiscriminated Unions
Discriminated Unions (tagged unions) verwenden ein gemeinsames Literal-Feld ('type', 'kind', 'tag'), um Varianten zu unterscheiden. TypeScript grenzt den Typ in jedem switch-Case ein und gibt Zugriff auf case-spezifische Felder. Der 'never'-Default ermöglicht Erschöpfungsprüfung – wenn du einen neuen Case hinzufügst, error der Compiler, bis du ihn behandelst. Essenziell für Redux-Reducer und Zustandsautomaten.
// Discriminated union: shared 'type' (or 'kind') field
type Action =
| { type: "ADD_TODO"; text: string }
| { type: "DELETE_TODO"; id: number }
| { type: "TOGGLE_TODO"; id: number };
function reducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case "ADD_TODO":
return [...state, { id: Date.now(), text: action.text }];
case "DELETE_TODO":
return state.filter(t => t.id !== action.id);
case "TOGGLE_TODO":
return state.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
);
default:
const _: never = action; // exhaustiveness check
return state;
}
}Type Guards: typeof & instanceof
Type Guards grenzen Typen zur Runtime ein. 'typeof' funktioniert für Primitive (string, number, boolean, symbol, bigint, undefined, function, object). 'instanceof' prüft Klassen-/Konstruktor-Prototypen. Array.isArray() grenzt zu einem typisierten Array ein. Dies sind eingebaute Guards – kein benutzerdefinierter Code nötig. TypeScript verfolgt den eingeengten Typ in jedem Zweig.
// typeof: narrow primitives
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // string
}
return value.toFixed(2); // number
}
// instanceof: narrow class instances
class Dog { bark() {} }
class Cat { meow() {} }
function speak(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // Dog
} else {
pet.meow(); // Cat
}
}
// Array.isArray
function flatten(arr: (number | number[])[]) {
return arr.flatMap(x =>
Array.isArray(x) ? x : [x]
);
}Benutzerdefinierte Type Guards & in-Operator
Der 'in'-Operator prüft, ob eine Eigenschaft auf einem Objekt existiert, und grenzt zu dem Typ ein, der sie hat. Type Predicates (x is T) sind benutzerdefinierte Guard-Funktionen, die Boolean zurückgeben, aber auch den Typ eingrenzen. Verwende 'unknown' als Eingabetyp für sicheres Parsen externer Daten (JSON.parse, API-Antworten). Predicates ermöglichen wiederverwendbare, komponierbare Typ-Prüfungen.
// 'in' operator: check for property existence
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // Fish
} else {
animal.fly(); // Bird
}
}
// Type predicate: custom guard function
function isString(x: unknown): x is string {
return typeof x === "string";
}
function isUser(x: any): x is { name: string; age: number } {
return x && typeof x.name === "string" && typeof x.age === "number";
}
const val: unknown = "hello";
if (isString(val)) {
console.log(val.toUpperCase()); // narrowed to string
}Datenstrukturen
Arrays & ReadonlyArray
TypeScript-Arrays sind typisiert – Methoden wie map, filter und reduce bewahren Elementtypen. Verwende readonly T[] oder ReadonlyArray<T> für Unveränderlichkeit. Tuples haben feste Länge und typisierte Positionen. Array-Destructuring und Spread sind vollständig typsicher. Das Typsystem fängt Index-out-of-bounds und falsch-typisierte Zuweisungen ab.
// Typed arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];
// ReadonlyArray (immutable)
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4); // Error: readonly
// Tuple (fixed length)
let pair: [string, number] = ["Alice", 30];
// Array methods are typed
const doubled = nums.map(n => n * 2); // number[]
const evens = nums.filter(n => n % 2 === 0); // number[]
const sum = nums.reduce((a, b) => a + b, 0); // number
// Spread and destructuring
const [first, ...rest] = nums;
const combined = [...nums, ...[4, 5]];Objekte & Records
Record<K, V> erstellt eine typisierte Map mit Schlüsseln vom Typ K und Werten vom Typ V. Partial<T> macht alle Eigenschaften optional – ideal für Update-/Patch-Operationen. Object.entries/keys/values geben typisierte Arrays zurück. Verwende Pick und Omit, um fokussierte Typen von existierenden abzuleiten und Typen DRY zu halten.
// Object type
const user: { name: string; age: number } = { name: "Alice", age: 30 };
// Record: typed key-value map
const scores: Record<string, number> = {
math: 90,
science: 85,
};
// Partial: all optional (for patches)
const patch: Partial<typeof user> = { age: 31 };
// Pick / Omit
type Summary = Pick<typeof user, "name">;
type WithoutAge = Omit<typeof user, "age">;
// Object.entries / keys / values typed
const entries = Object.entries(scores); // [string, number][]
const keys = Object.keys(scores); // string[]
const values = Object.values(scores); // number[]Maps & Sets
Map und Set sind ES6-Collections mit voller TypeScript-Unterstützung. Map-Schlüssel können beliebigen Typ sein (anders als Objekte, die Schlüssel zu Strings umwandeln). Set speichert eindeutige Werte. WeakMap/WeakSet erlauben Garbage Collection von Schlüsseln – nützlich für Metadaten, die an DOM-Elemente oder Objekte angehängt sind, ohne GC zu verhindern.
// Map: keyed collection (any key type)
const map = new Map<string, User>();
map.set("alice", { name: "Alice", age: 30 });
const user = map.get("alice"); // User | undefined
// Set: unique values
const unique = new Set<number>([1, 2, 2, 3]);
console.log(unique.size); // 3
console.log(unique.has(2)); // true
// Iteration (typed)
for (const [key, value] of map) {
console.log(key, value.name);
}
for (const num of unique) {
console.log(num);
}
// WeakMap / WeakSet (keys must be objects, GC-friendly)
const weak = new WeakMap<object, string>();Tuples & beschriftete Tuples
Tuples sind Arrays fester Länge mit typisierten Positionen. Beschriftete Tuples (TS 4.0+) fügen Namen für Lesbarkeit hinzu – nützlich für Rückgabewerte und CSV-ähnliche Daten. Tuples ermöglichen mehrfache Rückgabewerte ohne ein Interface zu erstellen. Verwende 'readonly', um Mutation zu verhindern. Tuples unterscheiden sich von Arrays: [string, number] ist NICHT (string | number)[].
// Basic tuple
let point: [number, number] = [10, 20];
// Labeled tuple (TS 4.0+)
let user: [id: number, name: string, active: boolean] = [1, "Alice", true];
// Destructuring with labels
const [id, name, active] = user;
// Tuple in function returns
function divmod(a: number, b: number): [quotient: number, remainder: number] {
return [Math.floor(a / b), a % b];
}
const [q, r] = divmod(17, 5);
console.log(q, r); // 3 2
// Readonly tuple
const fixed: readonly [string, number] = ["a", 1];
// fixed.push(2); // ErrorEnums & Const-Enums
Enums erstellen benannte Konstanten. String-Enums werden empfohlen (lesbar in Ausgabe, keine Reverse-Mapping-Probleme). Const-Enums werden zur Compile-Zeit gelöscht (zero Runtime-Cost). Für einfache Fälle sind Union-Typen ('a' | 'b') oft besser – kein Runtime-Code, besseres Tree-Shaking. Verwende Enums für gruppierte, dokumentierte Konstanten.
// Numeric enum
enum Direction { North = 0, South = 1, East = 2, West = 3 }
// String enum (recommended)
enum HttpStatus {
OK = "200 OK",
NotFound = "404 Not Found",
ServerError = "500 Internal Server Error",
}
// Const enum (inlined at compile time)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;
// Union type alternative (no runtime code)
type Status = "pending" | "success" | "error";
const s: Status = "pending";
// Exhaustive switch
function handle(s: Status) {
switch (s) {
case "pending": return "loading";
case "success": return "done";
case "error": return "failed";
}
}Unveränderliche Daten
TypeScript bietet mehrere Unveränderlichkeits-Tools: 'readonly' für Eigenschaften, ReadonlyArray für Arrays, Readonly<T>-Utility und 'as const' für tiefes readonly mit Literal-Typen. Unveränderliche Daten verhindern versehentliche Mutationen und ermöglichen Change Detection (React, Redux). Verwende Spread (...) für unveränderliche Updates – erstellt ein neues Objekt mit modifizierten Feldern.
// Readonly modifier
interface User {
readonly id: number;
name: string;
}
// ReadonlyArray
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums[0] = 9; // Error
// Readonly utility type
type FrozenUser = Readonly<User>;
// as const: deep readonly with literal types
const config = {
endpoint: "/api",
methods: ["GET", "POST"],
} as const;
// config.endpoint: "/api" (literal, not string)
// config.methods: readonly ["GET", "POST"]
// Immutable update pattern
const user = { name: "Alice", age: 30 };
const updated = { ...user, age: 31 }; // new objectModule & Namespaces
ES-Module: Import & Export
TypeScript verwendet ES-Modul-Syntax (import/export). Named-Exports sind explizit; Default-Export ist der einzelne 'Haupt'-Export. Verwende 'import * as' für Namespace-Imports. Modul-Auflösung folgt Node.js-Konventionen (node_modules, Erweiterungen). Konfiguriere 'module' und 'moduleResolution' in tsconfig.json.
// math.ts - exporting
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14159;
export default function multiply(a: number, b: number): number {
return a * b;
}
// main.ts - importing
import multiply, { add, PI } from "./math";
import * as math from "./math"; // namespace import
console.log(add(1, 2)); // 3
console.log(multiply(3, 4)); // 12 (default)
console.log(math.PI); // 3.14159Type-Only-Imports
'import type' importiert nur Typen (zur Compile-Zeit gelöscht, kein Runtime-Code). Das vermeidet zirkuläre Abhängigkeiten und unnötige Runtime-Imports. TS 4.5+ erlaubt inline 'type'-Modifikatoren in gemischten Imports. Verwende Type-Only-Imports für Interfaces, Type-Aliase und Enums (falls const), um die Bundle-Größe zu reduzieren.
// Type-only import (erased at runtime)
import type { User, Config } from "./types";
// Mixed import (TS 4.5+)
import { render, type Component } from "./ui";
// Re-export types
export type { User } from "./types";
// Import type for interfaces and types
interface UserService {
get(id: string): User; // User from type-only import
}
// 'import type' ensures no runtime dependency
// Useful when the module has side effects you want to avoidDynamische Imports
Dynamische Imports (import()) laden Module bei Bedarf und geben ein Promise zurück. Das ermöglicht Code-Splitting und Lazy Loading – kritisch für Performance in Web-Apps. TypeScript leitet den Modul-Typ automatisch ab. Verwende für optionale Features, große Bibliotheken und routenbasiertes Code-Splitting (React.lazy, Next.js dynamic).
// Dynamic import returns a Promise
async function loadModule() {
const module = await import("./heavy-module");
module.doWork();
}
// Conditional loading
if (featureEnabled) {
const { Feature } = await import("./feature");
new Feature().init();
}
// Type the dynamic import
type HeavyModule = typeof import("./heavy-module");
// With error handling
try {
const lib = await import("./optional-lib");
lib.run();
} catch (e) {
console.log("Library not available");
}Deklarationsdateien & Module-Augmentation
Deklarationsdateien (.d.ts) beschreiben Typen für JS-Module, CSS/PNG-Imports und globale Variablen. Module-Augmentation erweitert existierende Modul-Typen – nützlich, um Eigenschaften zu Express Request, Express Response oder Third-Party-Typen hinzuzufügen. So fügt Middleware wie passport req.user-Typisierung hinzu.
// global.d.ts - declare ambient modules
declare module "*.css" {
const content: string;
export default content;
}
declare module "*.png" {
const src: string;
export default src;
}
// Module augmentation - extend existing types
import express from "express";
declare module "express" {
interface Request {
user?: { id: string; name: string };
}
}
// Now req.user is typed
app.get("/", (req, res) => {
console.log(req.user?.name);
});Namespaces (Legacy)
Namespaces sind TypeScripts Pre-ES6-Modulsystem. Sie gruppieren verwandten Code unter einem benannten Objekt. Für neue Projekte bevorzuge ES-Module (import/export) – sie sind standardisiert, tree-shakeable und funktionieren mit Bundlern. Namespaces bleiben in .d.ts-Deklarationsdateien für globale Typdeklarationen und Legacy-Code nützlich.
// Namespace: pre-ES6 way to organize code
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isValid(s: string): boolean {
return lettersRegexp.test(s);
}
}
}
// Usage
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello")); // true
// Prefer ES modules over namespaces for new code
// Namespaces are useful in .d.ts files for global declarationstsconfig-Module-Einstellungen
tsconfig-Module-Einstellungen kontrollieren, wie TypeScript Imports behandelt. 'moduleResolution: node' verwendet Node.js-Auflösung (node_modules-Lookup). 'esModuleInterop' ermöglicht Default-Imports aus CommonJS. 'paths' erstellt Import-Aliase (@/components) für sauberere Imports. 'resolveJsonModule' erlaubt das Importieren von .json-Dateien mit abgeleiteten Typen.
{
"compilerOptions": {
"module": "ESNext", // ES module output
"moduleResolution": "node", // Node-style resolution
"esModuleInterop": true, // allow default imports from CJS
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true, // import .json files
"isolatedModules": true, // each file is independent
"baseUrl": "./src", // base for non-relative imports
"paths": {
"@/*": ["./*"], // path alias
"@components/*": ["./components/*"]
}
}
}
// With paths config, you can import:
// import { Button } from "@/components/Button";
// instead of relative paths like "../../components/Button"Async & Promises
Promise-Typen
Promise<T> ist der Kern-Async-Typ – T ist der aufgelöste Werttyp. TypeScript leitet Typen durch .then()-Ketten ab. Verwende 'new Promise()' für das Wrappen von Callback-basierten APIs. Typisiere immer die resolve/reject-Werte. Bevorzuge async/await gegenüber rohen .then()-Ketten für Lesbarkeit und Fehlerbehandlung.
// Promise<T> represents an async value
function fetchUser(id: number): Promise<User> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 1) {
resolve({ id: 1, name: "Alice" });
} else {
reject(new Error("User not found"));
}
}, 100);
});
}
// Consuming a Promise
fetchUser(1)
.then(user => console.log(user.name))
.catch(err => console.error(err))
.finally(() => console.log("done"));
// Promise typing: then callbacks are typed
fetchUser(1).then(user => {
// user is User, not any
console.log(user.name); // string
});async/await
async/await ist syntaktischer Zucker über Promises – 'await' pausiert, bis das Promise auflöst. Async-Funktionen geben immer ein Promise zurück. Verwende Promise.all() für parallele Ausführung (deutlich schneller als sequenzielle awaits). Top-Level-await funktioniert in ES-Modulen mit ES2022+. TypeScript prüft, dass awaited-Werte Promises sind.
// async function returns Promise<T>
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json() as Promise<User>;
}
// Top-level await (ES2022, TS 4.7+)
// const user = await getUser(1);
// Sequential vs parallel
async function loadAll() {
// Sequential (slow)
const a = await getUser(1);
const b = await getUser(2);
// Parallel (fast)
const [x, y] = await Promise.all([getUser(1), getUser(2)]);
return { a, b, x, y };
}Fehlerbehandlung in Async
Verwende try/catch mit async/await für Fehlerbehandlung – sauberer als .catch(). TypeScript unterstützt keine typisierten throws (alle Errors sind unknown in catch), also grenze mit instanceof ein. Für vorhersehbare Errors ziehe das Result-Typ-Pattern (ok/error-Union) anstelle von Exceptions in Betracht – es macht Fehlerbehandlung in der Typ-Signatur explizit.
// Try/catch with async/await
async function riskyOperation(): Promise<string> {
try {
const data = await fetch("/api/data");
if (!data.ok) throw new Error(`HTTP ${data.status}`);
return await data.text();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
return "fallback";
} finally {
console.log("cleanup");
}
}
// Typed errors (TypeScript doesn't have typed throws)
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
// Result type as alternative to exceptions
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
async function safeFetch(url: string): Promise<Result<string>> {
try {
const res = await fetch(url);
return { ok: true, value: await res.text() };
} catch (e) {
return { ok: false, error: String(e) };
}
}Promise-Combinators
Promise-Combinators orchestrieren mehrere Async-Operationen: all() (parallel, fail-fast), allSettled() (parallel, auf alle warten), race() (erste settled), any() (erste erfolgreich). Verwende all() für abhängiges Datenladen, allSettled(), wenn du Teilergebnisse willst, race() für Timeouts, any() für redundante Fetches.
// Promise.all: wait for all (rejects if any rejects)
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts(),
]);
// Promise.allSettled: wait for all (never rejects)
const results = await Promise.allSettled([
fetch("/api/a"),
fetch("/api/b"),
]);
results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value);
else console.log(r.reason);
});
// Promise.race: first to settle (resolve or reject)
const fastest = await Promise.race([
fetch("/api/fast"),
fetch("/api/slow"),
]);
// Promise.any: first to resolve (ignores rejections)
const first = await Promise.any([
fetch("/api/primary"),
fetch("/api/fallback"),
]);Event-Loop & Microtasks
JavaScripts Event-Loop verarbeitet Microtasks (Promise-Callbacks, queueMicrotask) vor Macrotasks (setTimeout, setInterval). Deshalb resolved Promises vor Timeouts. Async-Iteration (for await...of) konsumiert Async-Iterables – nützlich für Streams. Async-Generatoren (async function*) produzieren Async-Iterables und ermöglichen Lazy-Async-Sequenzen.
// Microtasks (Promises) run before macrotasks (setTimeout)
console.log("1: sync");
setTimeout(() => console.log("4: macrotask"), 0);
Promise.resolve().then(() => console.log("3: microtask"));
console.log("2: sync");
// Output: 1, 2, 3, 4
// queueMicrotask for custom microtasks
queueMicrotask(() => console.log("microtask"));
// Async iteration (for await)
async function processStream(stream: AsyncIterable<Buffer>) {
for await (const chunk of stream) {
console.log(chunk.length);
}
}
// Async generators
async function* generate() {
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 100));
yield i;
}
}Nebenläufigkeits-Patterns
Diese Patterns sind vollständig typsicher in TypeScript. Debounce verzögert die Ausführung, bis Aufrufe für N ms aufhören (Sucheingabe). Throttle begrenzt auf einen Aufruf pro N ms (Scroll-Handler). Semaphore/mapLimit kontrolliert Nebenläufigkeit – nützlich für rate-limited APIs. Parameters<T> und ReturnType<T> bewahren Funktions-Signaturen in Wrappern.
// Debounce with TypeScript
function debounce<T extends (...args: any[]) => void>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Throttle
function throttle<T extends (...args: any[]) => void>(
fn: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Semaphore (limit concurrency)
async function mapLimit<T, U>(
items: T[],
limit: number,
fn: (item: T) => Promise<U>
): Promise<U[]> {
const results: U[] = [];
const executing: Promise<void>[] = [];
for (const item of items) {
const p = fn(item).then(r => results.push(r));
executing.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
executing.splice(executing.findIndex(e => e === p), 1);
}
}
await Promise.all(executing);
return results;
}Fehlerbehandlung & Testing
Try/Catch mit Unknown
Seit TypeScript 4.4 (useUnknownInCatchVariables) sind gefangene Errors 'unknown' – du musst sie vor der Verwendung eingrenzen. Das verhindert den Zugriff auf nicht existierende Eigenschaften. Verwende instanceof, um nach spezifischen Error-Typen zu prüfen, oder String() als Fallback. Erstelle einen getErrorMessage()-Helfer für konsistente Error-Extraktion.
// In strict mode, catch is 'unknown' (not 'any')
try {
JSON.parse("invalid");
} catch (error: unknown) {
// Must narrow before using
if (error instanceof SyntaxError) {
console.error("JSON error:", error.message);
} else if (error instanceof Error) {
console.error(error.message);
} else {
console.error("Unknown error:", error);
}
}
// Helper function
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
return String(error);
}Benutzerdefinierte Error-Klassen
Benutzerdefinierte Error-Klassen fügen strukturierte Daten (Code, statusCode, field) zu Errors hinzu. Rufe immer super(message) auf und setze den Prototyp (Object.setPrototypeOf), um das TypeScript/ES5-Prototyp-Ketten-Problem zu beheben. Verwende instanceof, um Error-Typen in catch-Blöcken zu unterscheiden. Dieses Pattern ist essenziell für Express-Middleware und API-Error-Handler.
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500
) {
super(message);
this.name = "AppError";
// Fix prototype chain (TS quirk)
Object.setPrototypeOf(this, AppError.prototype);
}
}
class ValidationError extends AppError {
constructor(message: string, public field: string) {
super(message, "VALIDATION_ERROR", 400);
this.name = "ValidationError";
}
}
// Usage
function createUser(input: unknown) {
if (typeof input !== "object" || input === null) {
throw new ValidationError("Invalid input", "body");
}
}
try {
createUser("bad");
} catch (e) {
if (e instanceof ValidationError) {
console.log(e.field, e.statusCode); // "body" 400
}
}Result-Typ-Pattern
Der Result-Typ (von Rust) macht Errors in der Typ-Signatur explizit – Aufrufer müssen sowohl Erfolg als auch Fehlschlag behandeln. Anders als Exceptions erzwingt der Compiler Fehlerbehandlung. Verwende dies für erwartete Fehlschläge (Validierung, not-found), wo Exceptions übertrieben wären. Reserviere Exceptions für wirklich unerwartete Errors (Bugs, Systemfehler).
// Result type: explicit error handling without exceptions
type Result<T, E = string> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return { ok: false, error: "Division by zero" };
}
return { ok: true, value: a / b };
}
// Usage: forced to handle both cases
const result = divide(10, 0);
if (result.ok) {
console.log(result.value); // number
} else {
console.error(result.error); // string
}
// Utility helpers
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}Assertion-Funktionen
Assertion-Funktionen (asserts X) werfen, wenn eine Bedingung fehlschlägt, UND grenzen den Typ danach ein. 'asserts value is string' sagt TypeScript, dass nach dem Aufruf value string ist. Das ist sauberer als wiederholte If-Prüfungen. Verwende für Runtime-Validierung an Grenzen (API-Eingabe, Config). Kombiniere mit Zod oder io-ts für Schema-Validierung.
// Assertion function: throws if condition is false
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
// Assert value is a specific type
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
// Usage: narrows type after assertion
const input: unknown = "hello";
assertString(input);
console.log(input.toUpperCase()); // input is now string
// Assert non-null
function assertDefined<T>(value: T | undefined): asserts value is T {
if (value === undefined) throw new Error("undefined");
}Typsicheres JSON-Parsing
JSON.parse gibt 'any' zurück – unsicher. Wrappe es mit einem Type Guard, um die Form zur Runtime zu validieren und den Typ einzugrenzen. Für komplexe Schemas verwende Zod, io-ts oder yup – sie generieren sowohl Runtime-Validatoren als auch TypeScript-Typen aus einer einzigen Schema-Definition. Kritisch für API-Antworten und Benutzereingaben.
// Safe JSON parse with type guard
function safeParse<T>(json: string, guard: (x: unknown) => x is T): T | null {
try {
const parsed: unknown = JSON.parse(json);
if (guard(parsed)) return parsed;
return null;
} catch {
return null;
}
}
// Type guard for User
interface User { id: number; name: string; }
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null
&& typeof (x as User).id === "number"
&& typeof (x as User).name === "string";
}
const data = safeParse('{"id":1,"name":"Alice"}', isUser);
if (data) {
console.log(data.name); // User
}
// Using Zod for runtime validation
// import { z } from "zod";
// const UserSchema = z.object({ id: z.number(), name: z.string() });
// const user = UserSchema.parse(JSON.parse(json));Erschöpfungsprüfung
Erschöpfungsprüfung stellt sicher, dass du alle Cases einer Union behandelst. Weise den Default-Case 'never' zu – wenn du eine neue Variante zur Union hinzufügst, errort TypeScript, weil der neue Typ nicht 'never' zuweisbar ist. Das fängt fehlende Cases zur Compile-Zeit ab. Essenziell für discriminated unions, Redux-Reducer und Zustandsautomaten.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; w: number; h: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.w * shape.h;
default:
// If a new shape is added, this errors
const _exhaustive: never = shape;
return _exhaustive;
}
}
// Adding a new variant:
// | { kind: "triangle"; base: number; height: number }
// Now the default case errors: Type 'triangle' is not assignable to 'never'Decorators & Metadaten
Klassen-Decorators
Klassen-Decorators empfangen die Konstruktorfunktion und können eine modifizierte Klasse zurückgeben. Decorator-Fabriken (geben eine Funktion zurück) akzeptieren Argumente. Decorators sind ein experimentelles Feature – aktiviere 'experimentalDecorators' in tsconfig. Stark verwendet in NestJS, TypeORM und Angular für Dependency Injection und Metadaten.
// Class decorator: receives the constructor
function Logged<T extends new (...args: any[]) => any>(target: T): T {
return class extends target {
constructor(...args: any[]) {
super(...args);
console.log(`Created ${target.name}`);
}
};
}
@Logged
class Service {
constructor(public name: string) {}
}
const s = new Service("Auth");
// Logs: "Created Service"
// Decorator factory (with arguments)
function Prefix(prefix: string) {
return function <T extends new (...args: any[]) => any>(target: T): T {
return class extends target {
message = prefix + " " + (this as any).name;
};
};
}Methoden- & Property-Decorators
Methoden-Decorators empfangen (target, propertyKey, descriptor) und können die Originalmethode wrappen – nützlich für Logging, Caching und Zugriffskontrolle. Property-Decorators empfangen (target, key) und werden oft verwendet, um Metadaten zu registrieren. Der descriptor.value ist die Originalfunktion; wrappe sie, um Verhalten hinzuzufügen. Häufig in NestJS (@Get, @Post) und TypeORM (@Column).
// Method decorator: (target, key, descriptor)
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey}(${args})`);
return original.apply(this, args);
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
new Calculator().add(2, 3);
// Logs: "Calling add(2,3)"
// Property decorator: (target, key)
function Required(target: any, key: string) {
// Store metadata for validation
console.log(`Required: ${key}`);
}Parameter-Decorators & Metadaten
Parameter-Decorators empfangen (target, key, index) und werden für Dependency Injection (NestJS, Angular) verwendet. Der 'reflect-metadata'-Polyfill ermöglicht Runtime-Typ-Metadaten – Decorators können auf Parametertypen über Reflect.getMetadata('design:paramtypes') zugreifen. So wissen DI-Container, was zu injizieren ist. Aktiviere mit 'emitDecoratorMetadata: true' in tsconfig.
import "reflect-metadata";
// Parameter decorator
function Inject(target: any, key: string, index: number) {
console.log(`Inject param ${index} of ${key}`);
}
class Service {
constructor(@Inject private dep: any) {}
}
// Store and retrieve metadata
const METADATA_KEY = "design:type";
class Example {
greet(name: string): void {}
}
// reflect-metadata provides:
// - design:type (property type)
// - design:paramtypes (method parameter types)
// - design:returntype (method return type)
const types = Reflect.getMetadata("design:paramtypes", Example.prototype, "greet");
// types: [String]Accessor-Decorators
Accessor-Decorators gelten für Getter/Setter. Der Descriptor hat get/set-Eigenschaften, die du wrappen kannst. Verwende sie für Validierung, Logging oder Änderung der Enumerabilität. Das Validierungs-Pattern (MaxLength, Min, Max) wrappt den Setter, um Constraints zur Runtime durchzusetzen. So funktioniert class-validator (NestJS) für DTO-Validierung.
// Accessor decorator (getter/setter)
function Enumerable(value: boolean) {
return function (
target: any,
key: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value;
};
}
class Person {
private _name: string = "";
@Enumerable(true)
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
const p = new Person();
p.name = "Alice";
console.log(Object.keys(p)); // ["name"] (enumerable)
// Validation decorator
function MaxLength(len: number) {
return function (target: any, key: string) {
let value: string;
Object.defineProperty(target, key, {
get() { return value; },
set(v: string) {
if (v.length > len) throw new Error("Too long");
value = v;
},
});
};
}Moderne Decorators (TC39 Stage 3)
TypeScript 5.0 unterstützt den TC39-Stage-3-Decorator-Vorschlag – eine standardisierte API, die experimentalDecorators ersetzt. Die neue API verwendet ein Context-Objekt (ClassMethodDecoratorContext) anstelle von (target, key, descriptor). Sie ist sauberer, typsicher und wird schließlich im JS-Standard sein. Verwende dies für neue Projekte; experimentelle Decorators bleiben für NestJS/Angular-Kompatibilität.
// TC39 Stage 3 decorators (TS 5.0+, no experimentalDecorators needed)
// Uses a different API: context object
function log<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
return function (this: This, ...args: Args): Return {
console.log(`Calling ${String(context.name)}`);
return target.call(this, ...args);
};
}
class Service {
@log
greet(name: string): string {
return `Hello, ${name}`;
}
}
// Class method decorator context provides:
// - name: property name
// - kind: "method" | "field" | "class"
// - access: { get(), set(value) } for fields
// - addInitializer(): run code after constructionPraktischer Decorator: Memoize
Dieser memoize-Decorator cached Methodenergebnisse basierend auf Argumenten – drastische Beschleunigung für teure pure Functions wie fibonacci. Der Cache ist pro Instanz (verwende eine WeakMap für geteilten Cache). Decorators glänzen für Querschnittsthemen: Logging, Caching, Validierung, Zugriffskontrolle, Retry-Logik. Sie halten Geschäftslogik sauber, indem sie Infrastruktur-Themen trennen.
// Memoize: cache method results
function Memoize<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext
) {
const cache = new Map<string, Return>();
return function (this: This, ...args: Args): Return {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = target.call(this, ...args);
cache.set(key, result);
return result;
};
}
class MathService {
@Memoize
fibonacci(n: number): number {
if (n < 2) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const svc = new MathService();
console.time("first");
console.log(svc.fibonacci(40)); // slow
console.timeEnd("first");
console.time("second");
console.log(svc.fibonacci(40)); // fast (cached)
console.timeEnd("second");Utility-Typen
Partial, Required & Readonly
Partial<T> macht alle Eigenschaften optional – perfekt für Update-/Patch-Operationen, bei denen nur einige Felder ändern. Required<T> ist die Umkehrung. Readonly<T> macht alle Eigenschaften zur Compile-Zeit unveränderlich. Dies sind die am häufigsten verwendeten Utility-Typen und eliminieren die Notwendigkeit, parallele optionale/readonly-Interfaces manuell zu pflegen.
interface User {
id: number;
name: string;
email: string;
}
// Partial<T>: all properties become optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string }
function updateUser(id: number, changes: UserUpdate) {
// Only provided fields are updated
Object.assign(users[id], changes);
}
updateUser(1, { name: "Alice" }); // OK — only name
// Required<T>: all properties become required (inverse of Partial)
type StrictUser = Required<Partial<User>>; // back to all required
// Readonly<T>: all properties become readonly
type FrozenUser = Readonly<User>;
const frozen: FrozenUser = { id: 1, name: "Alice", email: "[email protected]" };
// frozen.name = "Bob"; // Error: readonly
// Practical: immutable config objects
const config: Readonly<Config> = { /* ... */ };
// config.port = 8081; // Error — prevents accidental mutationPick, Omit & Record
Pick<T, K> extrahiert eine Teilmenge von Eigenschaften; Omit<T, K> entfernt Eigenschaften – beide erstellen abgeleitete Typen ohne Duplizierung. Record<K, V> erstellt einen Map-/Dictionary-Typ mit spezifischen Schlüsseln. Essenziell für DTOs (Data Transfer Objects): Leite einen CreateUser-Typ von User ab, indem du auto-generierte Felder wie id und createdAt auslässt. Das hält Typen DRY und synchron.
interface User {
id: number;
name: string;
email: string;
role: string;
createdAt: Date;
}
// Pick<T, Keys>: select specific properties
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string }
// Omit<T, Keys>: remove specific properties
type UserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; role: string }
// Record<Keys, Value>: object with specific keys and value type
type UserRole = "admin" | "user" | "guest";
type Permissions = Record<UserRole, string[]>;
const perms: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
// Combining: create a DTO from a full entity
type UserDTO = Pick<User, "id" | "name" | "email">;
type CreateUserDTO = Omit<User, "id" | "createdAt">;ReturnType, Parameters & Awaited
ReturnType und Parameters extrahieren Typen aus existierenden Funktionen – unbezahlbar beim Wrappen oder Aufrufen von Funktionen, deren Signaturen du nicht duplizieren willst. Awaited<T> entpackt verschachtelte Promises (Promise<Promise<T>> wird zu T), essenziell für Async-Funktions-Rückgabetypen. InstanceType bekommt den Instanztyp von einem Klassen-Konstruktor. Diese ermöglichen typsichere Funktionskomposition und Higher-Order-Utilities.
function fetchUser(id: number): Promise<{ name: string; age: number }> {
return Promise.resolve({ name: "Alice", age: 30 });
}
// ReturnType<T>: the return type of a function
type FetchResult = ReturnType<typeof fetchUser>;
// Promise<{ name: string; age: number }>
// Awaited<T>: unwrap a Promise to its inner type
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number }
// Parameters<T>: tuple of parameter types
type FetchParams = Parameters<typeof fetchUser>;
// [id: number]
// First parameter type
type FirstParam = Parameters<typeof fetchUser>[0]; // number
// ConstructorParameters<T>: parameters of a class constructor
class Point {
constructor(public x: number, public y: number) {}
}
type PointArgs = ConstructorParameters<typeof Point>; // [x: number, y: number]
// InstanceType<T>: the instance type of a constructor
type PointInstance = InstanceType<typeof Point>; // PointExclude, Extract & NonNullable
Exclude<T, U> entfernt Typen aus einer Union; Extract<T, U> behält nur übereinstimmende Typen – beide operieren auf Union-Membern. NonNullable<T> entfernt null und undefined. Dies sind Bausteine: Omit ist definiert als Pick<T, Exclude<keyof T, K>>. Verwende Exclude/Extract, um Union-Typen dynamisch zu filtern, z.B. um Error-Typen von Success-Typen in einer Result-Union zu trennen.
type Role = "admin" | "user" | "guest" | "superadmin";
// Exclude<T, U>: remove types from a union
type NonAdmin = Exclude<Role, "admin" | "superadmin">;
// "user" | "guest"
// Extract<T, U>: keep only matching types from a union
type AdminRoles = Extract<Role, "admin" | "superadmin">;
// "admin" | "superadmin"
// NonNullable<T>: remove null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User
// Practical: filter union types
type EventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
scroll: UIEvent;
};
type EventName = keyof EventMap; // "click" | "keydown" | "scroll"
// Omit<T, K> is actually built from Pick and Exclude:
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;Benutzerdefinierte Utility-Typen
Benutzerdefinierte Utility-Typen komponieren eingebaute für spezifische Bedürfnisse. Optional<T, K> macht nur bestimmte Felder optional (gezielter als Partial). DeepPartial/DeepReadonly wenden rekursiv auf verschachtelte Objekte an – nützlich für Config und State-Trees. Der -readonly-Modifikator in Mutable entfernt readonly. Diese Patterns zeigen, wie Mapped Types und Conditional Types sich für mächtiges Typ-Level-Programming kombinieren.
// Make specific properties optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User {
id: number;
name: string;
email: string;
}
type UserWithOptionalEmail = Optional<User, "email">;
// { id: number; name: string; email?: string }
// Make specific properties required
type Require<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
// Deep partial (recursively make all properties optional)
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// Mutable (remove readonly from all properties)
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Non-never keys (filter out never-valued properties)
type NonNeverKeys<T> = {
[K in keyof T]: T[K] extends never ? never : K;
}[keyof T];Conditional Types
Basis-Conditional-Types (T extends U ? X : Y)
Conditional Types (T extends U ? X : Y) wählen einen Typ basierend auf einer Typ-Level-Bedingung – wie ein Ternary für Typen. Sie sind die Grundlage von TypeScripts Typ-Level-Programming. Wenn T eine Union ist, verteilt sich die Bedingung über jedes Member (distributive Conditional Types). Das infer-Schlüsselwort extrahiert Typen aus einem Pattern, wie den Elementtyp aus einem Array oder den Resolve-Typ aus einem Promise.
// Conditional types choose a type based on a condition
// Syntax: T extends U ? X : Y
// Is T an array? Return its element type, else never
type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<number[]>; // number
type C = ElementOf<string>; // never (not an array)
// Is T a Promise? Unwrap it, else keep T
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type D = UnwrapPromise<Promise<number>>; // number
type E = UnwrapPromise<string>; // string (not a promise)
// Check if a type is a function
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
type F = IsFunction<() => void>; // true
type G = IsFunction<number>; // false
// Conditional types are evaluated lazily and distribute over unionsinfer-Schlüsselwort (Typ-Extraktion)
Das infer-Schlüsselwort deklariert eine Typvariable innerhalb der extends-Klausel eines Conditional Types und erfasst, welcher Typ an dieser Position matcht. So sind ReturnType, Parameters und Awaited implementiert. infer kann rekursiv verwendet werden (Unwrap<Promise<Promise<T>>>), um verschachtelte Typen vollständig zu entpacken. Es ist das primäre Werkzeug, um Typen aus komplexen generischen Strukturen zu extrahieren.
// infer extracts a type from within a pattern
// Get the return type of a function (like ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Get the first parameter type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FA = FirstArg<(name: string, age: number) => void>; // string
// Get the resolved value of a Promise (like Awaited)
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Deep = Unwrap<Promise<Promise<Promise<number>>>>; // number (recursive!)
// Extract the value type from a Map
type MapValue<M> = M extends Map<any, infer V> ? V : never;
type MV = MapValue<Map<string, number>>; // number
// Extract element type from Set
type SetElement<S> = S extends Set<infer E> ? E : never;
// Get the instance type from a constructor
type Instance<T> = T extends new (...args: any[]) => infer I ? I : never;Distributive Conditional Types
Conditional Types verteilen sich über Unions: ToArray<A | B> anzuwenden ergibt ToArray<A> | ToArray<B>, nicht (A | B)[]. So filtern Exclude und NonNullable Union-Member – sie geben 'never' für ausgeschlossene Typen zurück, was in der Union kollabiert. Um Verteilung zu verhindern, wrappe beide Seiten in Klammern: [T] extends [U]. Verteilung ist meistens, was du zum Filtern willst, aber nicht-distributiv wird für 'die ganze Union wrappen'-Operationen benötigt.
// Conditional types DISTRIBUTE over unions
// T extends U ? X : Y applied to A | B becomes
// (A extends U ? X : Y) | (B extends U ? X : Y)
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// ToArray<string> | ToArray<number>
// = string[] | number[]
// WITHOUT distribution (wrap in brackets to prevent):
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[] — a single array of the union
// Practical: filter out types from a union
type ExcludeNull<T> = T extends null | undefined ? never : T;
type Cleaned = ExcludeNull<string | null | number | undefined>;
// string | number (null and undefined filtered out)
// This is exactly how NonNullable<T> works:
// type NonNullable<T> = T extends null | undefined ? never : T;Conditional-Type-Constraints
Conditional Types können verschachtelt werden, um Typ-Level-Diskriminierung zu erstellen (wie eine switch-Anweisung für Typen). Kombiniert mit infer extrahieren und leiten sie Typen von generischen Parametern ab. So leiten Bibliotheken wie React Prop-Typen von Komponenten-Definitionen ab, und wie Routing-Bibliotheken Parametertypen aus Pfad-Strings extrahieren. Die Constraint (T extends any[]) stellt sicher, dass die Eingabe gültig ist, bevor der Conditional den Elementtyp extrahiert.
// Use conditional types to constrain and derive types
// Get the props type of a React component
type PropsOf<C> = C extends React.ComponentType<infer P> ? P : never;
type ButtonProps = PropsOf<typeof Button>; // the component's prop type
// Conditional with multiple branches (nested)
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T1 = TypeName<string>; // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{}>; // "object"
// Constraint + conditional: only allow arrays, get element type
function firstElement<T extends any[]>(arr: T): T extends (infer E)[] ? E : never {
return arr[0] as any;
}
const x: number = firstElement([1, 2, 3]); // numberTemplate-Literal-Typen (String-Manipulation)
Template-Literal-Typen ermöglichen Typ-Level-String-Manipulation – Konkatenation, Groß-/Kleinschreibungskonvertierung und Pattern-Matching. Kombiniert mit Conditional Types und infer können sie Pfad-Strings parsen, um Route-Parameter zu extrahieren, Event-Handler-Namen generieren oder typsichere Property-Accessors bauen. So erstellen Frameworks wie Next.js und tRPC End-to-End-typsichere APIs aus String-Literalen.
// Template literal types: type-level string operations
type Greeting = `Hello ${string}`;
const g: Greeting = "Hello World"; // OK
// const bad: Greeting = "Hi World"; // Error
// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"foo">; // "Foo"
// Build event handler names from event names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Extract route parameters
type ExtractParams<Path extends string> =
Path extends `${infer _Start}/${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${infer _Start}/${infer Param}`
? Param
: never;
type Params = ExtractParams<"/users/:id/posts/:postId">;
// ":id" | ":postId"
// Property accessor type: "a.b.c" -> nested type
type Get<T, P extends string> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T ? Get<T[Key], Rest> : never
: P extends keyof T ? T[P] : never;Mapped Types
Basis-Mapped Types
Mapped Types iterieren über die Schlüssel eines Objekts und transformieren jede Eigenschaft – [K in keyof T] ist die Syntax. So sind Partial, Readonly, Pick und andere Utility-Typen implementiert. Du kannst den Eigenschaftstyp modifizieren (T[K] | null), Modifikatoren hinzufügen (? oder readonly) oder den Werttyp vollständig ersetzen. Mapped Types sind das Rückgrat von TypeScripts Typ-Transformations-System.
// Mapped types transform each property of an existing type
// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
id: number;
name: string;
email: string;
}
type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }
// Change all property types to a specific type
type Stringify<T> = {
[K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { id: string; name: string; email: string }Key-Remapping via 'as'
Key-Remapping (as-Klausel, TS 4.1+) lässt dich Schlüssel während des Mappings umbenennen oder filtern. Verwende Template-Literal-Typen, um Schlüsselnamen zu transformieren (Präfixe hinzufügen, zu Gettern konvertieren, Großschreibung). 'never' für einen Schlüssel zurückgeben entfernt ihn – so filterst du Eigenschaften. Kombiniert mit Conditional Types ermöglicht Key-Remapping mächtige Transformationen wie das Konvertieren eines Daten-Schemas in ein Validierungs-Schema oder eines API-Typs in einen Form-Typ.
// Remap keys using 'as' (TypeScript 4.1+)
// Add a prefix to all keys
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
interface User {
id: number;
name: string;
}
type PrefixedUser = Prefix<User, "user">;
// { userId: number; userName: string }
// Convert keys to getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }
// Filter out keys by returning never
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K];
};
type WithoutKind = RemoveKindField<{ kind: "circle"; radius: number }>;
// { radius: number }
// Make keys UPPERCASE
type UpperKeys<T> = {
[K in keyof T as Uppercase<string & K>]: T[K];
};Modifikatoren: +, -, ?, readonly
Die + und - Modifikatoren fügen Eigenschafts-Modifikatoren hinzu oder entfernen sie. -? entfernt Optionalität (macht optionale Felder erforderlich); -readonly entfernt Unveränderlichkeit. So funktionieren Required<T> und das Mutable-Pattern. Das +-Präfix ist optional (readonly ist dasselbe wie +readonly), aber - ist zum Entfernen erforderlich. Das gibt feinkörnige Kontrolle über Eigenschafts-Charakteristika während Typ-Transformationen.
// Add (+) or remove (-) modifiers
// Remove optional (?) modifier: -?
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
interface OptionalUser {
id?: number;
name?: string;
}
type RequiredUser = Concrete<OptionalUser>;
// { id: number; name: string } — all required now
// Remove readonly modifier: -readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
interface FrozenConfig {
readonly port: number;
readonly host: string;
}
type EditableConfig = Mutable<FrozenConfig>;
// { port: number; host: string } — mutable now
// Add readonly modifier: +readonly (or just readonly)
type Freeze<T> = {
+readonly [K in keyof T]: T[K];
};
// Add optional modifier: +?
type MakeOptional<T> = {
[K in keyof T]+?: T[K];
};Homomorphe Mapped Types
Homomorphe Mapped Types ([K in keyof T]) bewahren Eigenschafts-Modifikatoren (readonly, ?) vom Quelltyp – deshalb behält Pick<User, 'id'> id readonly. Nicht-homomorphe Mappings (z.B. [K in string]) bewahren keine Modifikatoren. Das ist wichtig beim Ableiten von Typen: Ein homomorphes Partial eines Typs mit readonly-Feldern behält diese Felder readonly (aber optional). Homomorphismus zu verstehen, hilft vorherzusagen, ob Modifikatoren eine Transformation überleben.
// Homomorphic mapped types preserve modifiers from the original type
interface User {
readonly id: number;
name: string;
email?: string;
}
// Homomorphic: [K in keyof T] — preserves readonly and ?
type Clone<T> = {
[K in keyof T]: T[K];
};
type ClonedUser = Clone<User>;
// { readonly id: number; name: string; email?: string }
// Modifiers are PRESERVED from User
// Non-homomorphic: doesn't preserve modifiers
type NonHomo<T> = {
[K in string]: T; // not keyof T — loses modifiers
};
// Pick is homomorphic — preserves modifiers
type UserPick = Pick<User, "id" | "name">;
// { readonly id: number; name: string } — readonly preserved!
// Practical: create a "patch" type that preserves optionality
type Patch<T> = {
[K in keyof T]?: T[K]; // Partial is homomorphic
};Einen Validierungs-Typ aus einem Schema bauen
So halten Form-Bibliotheken (React Hook Form, Formik) und Validierungsbibliotheken (Zod, Yup) Typsicherheit aufrecht – sie leiten Validator- und Form-Typen von deinen Daten-Interfaces mit Mapped Types ab. Wenn du ein Feld zu User hinzufügst, fordern der Validator und die Form-Typen es automatisch auch, was Drift verhindert. Das demonstriert die Real-World-Mächtigkeit von Mapped Types: Eine Quelle der Wahrheit (das Interface) treibt mehrere abgeleitete Typen.
// Practical: derive a validator type from a data type
// Given a data interface
interface User {
id: number;
name: string;
email: string;
}
// Create a validator type where each field is a validation function
type Validator<T> = {
[K in keyof T]: (value: T[K]) => boolean;
};
const userValidator: Validator<User> = {
id: (v) => v > 0,
name: (v) => v.length > 0,
email: (v) => v.includes("@"),
};
// Create a form type where fields are wrapped in a FormField
type FormField<T> = {
value: T;
error: string | null;
touched: boolean;
};
type Form<T> = {
[K in keyof T]: FormField<T[K]>;
};
const userForm: Form<User> = {
id: { value: 1, error: null, touched: false },
name: { value: "Alice", error: null, touched: true },
email: { value: "", error: "Required", touched: true },
};
// The form type is always in sync with User — add a field to
// User and the form type automatically requires it too.Type Guards & Narrowing
typeof- & instanceof-Narrowing
TypeScript grenzt Typen basierend auf Runtime-Prüfungen ein. typeof grenzt Primitive ein (string, number, boolean usw.); instanceof grenzt Klassen-Instanzen ein. Truthiness-Prüfungen (if (value)) grenzen null/undefined/0/''/false aus. Das Narrowing gilt innerhalb des Zweigs, wo die Bedingung gilt. So macht TypeScript Runtime-Prüfungen zu Typ-Information und eliminiert die Notwendigkeit expliziter Casts.
// typeof narrows primitive types
function padLeft(value: string | number, padding: string | number) {
if (typeof padding === "number") {
return " ".repeat(padding) + value;
// padding is narrowed to 'number' here
}
return padding + value;
// padding is narrowed to 'string' here
}
// instanceof narrows class types
class Cat { meow(): void {} }
class Dog { bark(): void {} }
function speak(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow(); // OK — narrowed to Cat
} else {
animal.bark(); // OK — narrowed to Dog
}
}
// typeof returns: "string" | "number" | "boolean" | "symbol"
// "bigint" | "undefined" | "object" | "function"
// Note: typeof null === "object" (historical JS bug)
// Truthiness narrowing
function process(value?: string) {
if (value) {
console.log(value.toUpperCase()); // value is string (not undefined)
}
}in-Operator & Discriminated Unions
Der 'in'-Operator grenzt basierend auf Eigenschafts-Existenz ein. Discriminated Unions verwenden eine gemeinsame Literal-Eigenschaft (wie 'kind' oder 'type') als Tag – Switchen darauf grenzt zur korrekten Variante mit vollem Eigenschaftszugriff ein. Das ist das TypeScript-Äquivalent von Summentypen / algebraischen Datentypen. Es ist das Standard-Pattern für Redux-Actions, Zustandsautomaten und API-Antworten mit mehreren Formen. Verwende immer einen Literal-Typ für das Diskriminans.
// 'in' operator checks for a property — narrows to types that have it
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // narrowed to Fish
} else {
animal.fly(); // narrowed to Bird
}
}
// Discriminated unions: a shared literal property (the 'discriminant')
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // narrowed by kind
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
// Discriminated unions are the idiomatic TS pattern for
// variant data (like Redux actions, AST nodes, API responses)Benutzerdefinierte Type-Guard-Funktionen
Benutzerdefinierte Type Guards (Rückgabetyp 'x is Type') lassen dich komplexe Runtime-Prüfungen in wiederverwendbare Funktionen kapseln, die Typen eingrenzen. Assertion-Funktionen (asserts x is T) werfen statt Boolean zurückzugeben – sie grenzen in allem Code nach dem Aufruf ein. Verwende Type Guards, um nicht vertrauenswürdige Daten (JSON.parse, API-Antworten) zu validieren und ins Typsystem zu bringen. Das überbrückt die Lücke zwischen Runtime-Validierung und Compile-Zeit-Typen.
// A type guard function has a special return type: 'x is Type'
// It narrows the type when it returns true
interface User {
id: number;
name: string;
}
function isUser(obj: any): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
typeof obj.id === "number" &&
typeof obj.name === "string"
);
}
const data: unknown = JSON.parse('{"id": 1, "name": "Alice"}');
if (isUser(data)) {
console.log(data.name); // data is narrowed to User
}
// Type guard for arrays
function isStringArray(arr: unknown): arr is string[] {
return Array.isArray(arr) && arr.every((x) => typeof x === "string");
}
// Type guard for discriminated unions
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function isSuccess<T>(r: Result<T>): r is { success: true; data: T } {
return r.success;
}
// Assertion functions (TS 3.7+): throw if condition fails
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error("Expected value to be defined");
}
}Erschöpfungsprüfung mit never
Erschöpfungsprüfung verwendet den 'never'-Typ, um sicherzustellen, dass alle Union-Varianten behandelt werden. Wenn du eine neue Variante zur Union hinzufügst, aber einen Case vergisst, wird die 'never'-Zuweisung im Default-Zweig zu einem Compile-Error. Der assertNever-Helfer wirft zur Runtime und errort zur Compile-Zeit für fehlende Cases. Das ist das wertvollste Pattern für discriminated unions – es lässt den Compiler dir sagen, wenn du einen Case vergessen hast.
// The 'never' type ensures all cases are handled
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default:
// If we add a new shape but forget a case, this line
// becomes a compile error because 'shape' would not be 'never'
const _exhaustive: never = shape;
return _exhaustive;
}
}
// Adding a new variant without updating area():
// type Shape = ... | { kind: "triangle"; base: number; height: number }
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'
// Helper function pattern
function assertNever(x: never): never {
throw new Error("Unexpected: " + JSON.stringify(x));
}
function process(shape: Shape) {
switch (shape.kind) {
case "circle": return;
case "square": return;
default: return assertNever(shape); // compile error if a case is missing
}
}Narrowing mit Array-Methoden
Array.filter grenzt Elementtypen standardmäßig nicht ein, weil sein Callback Boolean zurückgibt, kein Type Guard. Um einzugrenzen, übergib eine benutzerdefinierte Type-Guard-Funktion (pet is Dog) – dann gibt filter den eingeengten Array-Typ zurück. TypeScript grenzt auch innerhalb Callback-Bodies (forEach, map) basierend auf If-Prüfungen ein. Array.isArray ist ein eingebauter Type Guard, der unknown/any zu einem Array-Typ eingrenzt.
// TypeScript narrows through filter, map, and control flow
interface Pet {
name: string;
speak(): void;
}
class Dog implements Pet {
name = "Rex";
speak() { console.log("Woof"); }
fetch() { console.log("Fetching!"); }
}
class Cat implements Pet {
name = "Whiskers";
speak() { console.log("Meow"); }
}
const pets: Pet[] = [new Dog(), new Cat(), new Dog()];
// filter doesn't narrow by default (callback return type is boolean)
// Use a type guard to narrow:
function isDog(pet: Pet): pet is Dog {
return pet instanceof Dog;
}
const dogs = pets.filter(isDog); // Dog[] — properly narrowed!
dogs.forEach((d) => d.fetch()); // OK — d is Dog
// Narrowing in forEach/callbacks
pets.forEach((pet) => {
if (isDog(pet)) {
pet.fetch(); // narrowed to Dog inside callback
}
});
// Array.isArray narrows 'unknown' to an array
function process(input: unknown) {
if (Array.isArray(input)) {
input.length; // OK — input is any[] (or unknown[])
}
}Typ-Inferenz
Variablen- & Rückgabetyp-Inferenz
TypeScript leitet Typen von Initialisierern und Return-Statements ab, sodass du selten explizite Annotationen brauchst. Variablen weiten zu ihrem allgemeinen Typ auf (let x = 10 leitet number ab, nicht 10). 'as const' verhindert Widening: Es lässt Literale Literal bleiben, Objekte readonly und Arrays werden zu readonly Tuples. typeof colors[number] extrahiert eine Union von Tuple-Element-Typen – ein häufiges Pattern für das Ableiten Enum-ähnlicher Typen aus Arrays.
// TypeScript infers types when you don't annotate
// Variable inference
let count = 0; // number
let name = "Alice"; // string
let items = [1, 2, 3]; // number[]
let mixed = [1, "two"]; // (string | number)[]
// Function return type inference
function add(a: number, b: number) {
return a + b; // return type inferred as number
}
// const assertions for literal types
const x = 10; // number (widened)
const y = "hello"; // string (widened)
const z = { a: 1 }; // { a: number }
const x2 = 10 as const; // 10 (literal type)
const y2 = "hello" as const; // "hello" (literal type)
const z2 = { a: 1 } as const; // { readonly a: 10 }
// Array with 'as const' becomes a readonly tuple
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"Kontextuelle Typisierung
Kontextuelle Typisierung lässt den erwarteten Typ rückwärts in Ausdrücke fließen. Wenn du eine Funktion einer typisierten Variablen zuweist, werden die Parametertypen vom Zieltyp abgeleitet. Deshalb brauchen Event-Handler, Array-Callbacks und Objekt-Literale oft keine Typ-Annotationen. Die Daumenregel: Annotiere Funktions-Signaturen (Parameter und Rückgabetypen für öffentliche APIs), aber lass Inferenz Locals und Callbacks behandeln.
// Contextual typing: the expected type influences the inferred type
// Without context, the parameter type is unknown
window.onmousedown = (mouseEvent) => {
// mouseEvent is inferred as MouseEvent (from onmousedown's type)
console.log(mouseEvent.button);
};
// Array map: callback params inferred from array type
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2); // n is number, result is number[]
// Contextual typing for object literals
interface User {
name: string;
age: number;
}
const user: User = {
name: "Alice", // inferred as string (from User.name)
age: 30,
};
// Contextual typing with discriminated unions
type Event =
| { type: "click"; x: number; y: number }
| { type: "scroll"; scrollTop: number };
function handle(e: Event) {
if (e.type === "click") {
console.log(e.x, e.y); // narrowed
}
}
// Best practice: let inference work for you — don't over-annotate
const nums2 = [1, 2, 3].map((n) => n * 2); // number[] — no annotation neededBester gemeinsamer Typ (Union-Inferenz)
Beim Ableiten von mehreren Werten (wie Array-Literalen) findet TypeScript den 'besten gemeinsamen Typ' – meistens ein Supertyp oder eine Union. Ein Array von [Dog, Cat] leitet als Animal[] (die gemeinsame Basis) ab, nicht (Dog | Cat)[]. Um eine Union zu erhalten, annotiere explizit. Bedingte Returns leiten die Union aller Zweige ab. Das zu verstehen, hilft vorherzusagen, wann du explizite Annotationen brauchst vs. wann Inferenz ausreicht.
// When inferring from multiple values, TS finds the best common type
// Array of same type: inferred as that type
const nums = [1, 2, 3]; // number[]
// Array of different types: inferred as union
const mixed = [1, "two", true]; // (string | number | boolean)[]
// Array of subclasses: inferred as the common supertype
class Animal { name: string; }
class Dog extends Animal { bark(): void {} }
class Cat extends Animal { meow(): void {} }
const pets = [new Dog(), new Cat()]; // Animal[] (not (Dog | Cat)[])
// To get a union instead, use an explicit type annotation:
const pets2: (Dog | Cat)[] = [new Dog(), new Cat()];
// Or use 'as const' for readonly tuples:
const tuple = [new Dog(), new Cat()] as const;
// readonly [Dog, Cat]
// Return type inference with conditionals
function getValue(flag: boolean) {
return flag ? 42 : "hello"; // inferred as number | string
}Kontrollfluss-Analyse
TypeScript führt Kontrollfluss-Analyse durch – es verfolgt, wie Typen durch If/Else, Returns, Zuweisungen und logische Operatoren eingeengt und erweitert werden. Ein Typ wird nach einer Prüfung eingeengt und bleibt eingeengt, bis die Variable neu zugewiesen wird. Early Returns (Guard Clauses) sind besonders effektiv: Nach 'if (value === null) return' weiß der Rest der Funktion, dass value nicht null ist. Deshalb funktioniert Guard-Clause-Stil-Code so gut mit TypeScript.
// TypeScript tracks types through control flow
function example(value: string | number | null) {
// After this check, 'value' is string | number in this branch
if (value === null) {
return;
}
// value is now string | number (null narrowed out)
console.log(value);
if (typeof value === "string") {
return value.toUpperCase(); // value is string
}
// value is now number (string and null narrowed out)
return value.toFixed(2);
}
// Narrowing through assignments
let x: string | number;
x = "hello";
console.log(x.toUpperCase()); // x is string
x = 42;
console.log(x.toFixed(2)); // x is number
// Narrowing with && and ||
function process(input?: string) {
const value = input && input.trim(); // string | undefined
const safe = input || "default"; // string (undefined narrowed out)
}
// Narrowing is reset when a variable is reassigned
let v: string | number = "hi";
v.toUpperCase(); // string
v = 42;
// v.toUpperCase(); // Error: number doesn't have toUpperCasesatisfies-Operator (TS 4.9+)
Der 'satisfies'-Operator (TS 4.9+) validiert, dass ein Wert einem Typ entspricht, während der spezifischste abgeleitete Typ bewahrt wird – anders als Typ-Annotationen, die weiten. Ideal für Configs, Route-Maps und Theme-Objekte: Du bekommst Compile-Zeit-Validierung, dass die Struktur korrekt ist, aber Eigenschaftszugriffe geben noch den präzisen Literal-Typ zurück. Kombiniere mit 'as const' für sowohl Literal-Bewahrung als auch strukturelle Validierung.
// 'satisfies' checks a value matches a type WITHOUT widening it
// Problem: annotation widens the type
const config1: Record<string, string | number> = {
port: 8080,
host: "localhost",
};
// config1.port is 'string | number' (widened — lost the literal)
// Solution: 'satisfies' validates but preserves the inferred type
const config2 = {
port: 8080,
host: "localhost",
} satisfies Record<string, string | number>;
// config2.port is 'number' (preserved!) but still validated
// Practical: type-safe route configs
type Routes = Record<string, { method: string; handler: () => void }>;
const routes = {
"/users": { method: "GET", handler: () => fetchUsers() },
"/posts": { method: "POST", handler: () => createPost() },
} satisfies Routes;
routes["/users"].method; // string (from Routes)
routes["/users"].handler; // () => void (from Routes)
// 'as const' + 'satisfies': literal types + validation
const colors = {
primary: "#ff0000",
secondary: "#00ff00",
} as const satisfies Record<string, `#${string}`;Deklarationsdateien & Module-Augmentation
.d.ts-Deklarationsdateien schreiben
.d.ts-Dateien enthalten Typdeklarationen (keine Implementierung) – sie beschreiben die Typen von JavaScript-Code. Verwende 'declare module', um Typen für untypisierte npm-Pakete hinzuzufügen. 'declare global' erweitert globale Typen wie Window. Ambient-Deklarationen sagen TypeScript 'das existiert zur Runtime, vertrau mir'. So integrierst du Legacy-JS, Browser-APIs und Build-Zeit-injizierte Variablen ins Typsystem.
// types/my-module.d.ts — describes the types of a JS library
// Declare a module (for libraries without types)
declare module "untyped-lib" {
export function greet(name: string): string;
export const version: string;
export interface Config {
timeout: number;
retries: number;
}
}
// Ambient declarations for global variables
declare global {
interface Window {
myCustomProp: string;
myApp: { init: () => void };
}
}
// Now window.myCustomProp is typed
// window.myCustomProp; // string
// Declaring a global function
declare function myGlobalFn(x: number): string;
// Declaring a global namespace
declare namespace MyLib {
function doSomething(): void;
const version: string;
}
// Use 'declare' for things that exist at runtime but not in TS
// Common for: legacy JS, browser globals, build-time constantsModule-Augmentation (Existierende Typen erweitern)
Module-Augmentation erweitert existierende Typen von anderen Modulen – fügt Interfaces Eigenschaften hinzu, ohne die Originalquelle zu modifizieren. So fügt Express-Middleware (wie passport) req.user hinzu, und wie du Third-Party-Bibliotheks-Typen erweiterst. Die 'declare module'-Syntax öffnet den Typ-Raum des Moduls erneut. Augmentationen müssen in einem Modul (einer Datei mit import/export) sein, um global zu wirken.
// Module augmentation: add to an existing module's types
// Extend an interface from another module
import express from "express";
declare module "express" {
interface Request {
user?: {
id: number;
name: string;
};
// Now req.user is typed on all Express requests
}
}
// Augment a third-party module
declare module "axios" {
export interface AxiosRequestConfig {
retryCount?: number; // add a custom config option
}
}
// Augment a global interface
declare global {
interface Array<T> {
last(): T | undefined; // add a custom array method
}
}
// Implementation (in a .ts file, not .d.ts)
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3 — now typed!
// Module augmentation is how middleware adds typed properties
// to req/res objects in Express, Fastify, etc.Triple-Slash-Direktiven
Triple-Slash-Direktiven (///) sind spezielle Compiler-Kommentare, die TypeScript anweisen, zusätzliche Dateien oder Typ-Pakete einzuschließen. Die häufigste ist /// <reference types='node' /> für das Einschließen von @types/node. Mit modernen tsconfig.json 'types'- und 'lib'-Optionen werden diese selten benötigt – bevorzuge config-basierte Einstellungen. Sie sind hauptsächlich in .d.ts-Dateien und Legacy-Code zu sehen. Sie zu verstehen, hilft beim Lesen von Deklarationsdateien.
// Triple-slash directives are special comments processed by TS
/// <reference path="./other-types.d.ts" />
// Includes another declaration file (rarely needed with modules)
/// <reference types="node" />
// Includes types from @types/node (like process, Buffer, __dirname)
/// <reference lib="es2020" />
// Includes a built-in lib (alternative to tsconfig "lib")
// Most common use: referencing @types packages
// In a .d.ts file for a package that needs Node types:
/// <reference types="node" />
declare function readFile(path: string): Buffer; // Buffer from @types/node
// NOTE: With modern TS and tsconfig "types" and "lib" options,
// triple-slash directives are rarely needed. They're mostly
// used in .d.ts files for backward compatibility.
// Prefer tsconfig.json settings:
// {
// "compilerOptions": {
// "types": ["node"], // instead of /// <reference types="node" />
// "lib": ["es2020", "dom"] // instead of /// <reference lib="es2020" />
// }
// }Typen mit einem Paket veröffentlichen
Um TypeScript-Typen mit deinem npm-Paket zu veröffentlichen, setze 'types' in package.json auf deine .d.ts-Datei und aktiviere 'declaration: true' in tsconfig. Konsumenten bekommen automatisch Typen, wenn sie dein Paket installieren. declarationMap ermöglicht 'Go to Definition', um zur Quell-.ts-Datei zu springen. Für Bibliotheken ohne gebündelte Typen stellt das DefinitelyTyped-Projekt (@types/package) community-gepflegte Deklarationen bereit.
// package.json for a library with TypeScript types
{
"name": "my-library",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts", // <-- points to the type declarations
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
// tsconfig.json for building a library
{
"compilerOptions": {
"declaration": true, // generate .d.ts files
"declarationMap": true, // generate .d.ts.map (source maps for types)
"sourceMap": true, // generate .js.map
"outDir": "./dist",
"rootDir": "./src",
"composite": true // enable project references
}
}
// Consumers get types automatically when they 'npm install my-library'
import { myFunction } from "my-library"; // fully typed!
// For libraries without bundled types, install @types package:
// npm install --save-dev @types/express
// Check if types exist: https://www.typescriptlang.org/dt/searchType-Only-Imports & -Exports
'import type' importiert nur Typ-Information – es wird zur Runtime vollständig gelöscht, reduziert Bundle-Größe und vermeidet zirkuläre Abhängigkeitsprobleme. Verwende es für Interfaces, Type-Aliase und Type-Only-Re-Exports. Die inline 'import { x, type Y }'-Syntax (TS 4.5+) mischt Wert- und Typ-Imports sauber. verbatimModuleSyntax (TS 5.0+) erzwingt dies streng. Bevorzuge 'import type', wann immer du etwas importierst, das nur in Typ-Positionen verwendet wird.
// 'import type' imports only types (erased at runtime, no JS emitted)
import type { User, Config } from "./types";
import type { ReactNode } from "react";
// These are erased completely — no runtime import in the output JS
// Useful for: reducing bundle size, avoiding circular dependencies
// Mixed import: value + type
import { createStore, type Store } from "redux";
// createStore is a runtime import; Store is type-only
// 'export type' re-exports types only
export type { User, Config } from "./types";
// Interface vs Type for declarations
interface IUser { name: string; } // can be augmented/merged
type TUser = { name: string }; // cannot be merged, more flexible
// VerbatimModuleSyntax (TS 5.0+): enforces 'import type' for type-only
// imports, ensuring they're always elided
// {
// "compilerOptions": { "verbatimModuleSyntax": true }
// }tsconfig.json-Optionen
Kern-Compiler-Optionen
Die tsconfig.json kontrolliert, wie TypeScript kompiliert. 'target' setzt die Ausgabe-JS-Version; 'module' setzt das Modulsystem. 'strict: true' ist die wichtigste einzelne Einstellung – sie aktiviert alle strikten Typ-Prüfungen (noImplicitAny, strictNullChecks usw.). 'lib' bestimmt, welche eingebauten APIs verfügbar sind (DOM für Browser, ES2022 für moderne JS-Features). Beginne neue Projekte immer mit strict: true.
{
"compilerOptions": {
"target": "ES2022", // JS version to compile to
"module": "ESNext", // module system (ESNext, CommonJS, etc.)
"moduleResolution": "bundler", // how modules are resolved
"lib": ["ES2022", "DOM"], // available type definitions
"outDir": "./dist", // output directory for compiled JS
"rootDir": "./src", // root of source files
"sourceMap": true, // generate .map files for debugging
"declaration": true, // generate .d.ts files (for libraries)
"removeComments": true, // strip comments from output
// Type checking strictness
"strict": true, // enable ALL strict checks (recommended)
"noImplicitAny": true, // error on implicit 'any'
"strictNullChecks": true, // null/undefined are separate types
"noUnusedLocals": true, // error on unused local variables
"noUnusedParameters": true,// error on unused function params
"noImplicitReturns": true, // error if not all paths return
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true, // allow default imports from CommonJS
"skipLibCheck": true, // skip type checking of .d.ts files
"forceConsistentCasingInFileNames": true
}
}Strict-Mode-Flags erklärt
Strict Mode ist ein Bündel von Striktheits-Flags. strictNullChecks ist das wirkungsvollste – es macht null/undefined zu unterschiedlichen Typen und zwingt dich, sie explizit zu behandeln (die #1-Quelle von Runtime-Abstürzen). noImplicitAny verhindert stille Typ-Erosion. strictPropertyInitialization fängt uninitialisierte Klassen-Felder ab (verwende ! für Definite Assignment oder initialisiere im Konstruktor). Aktiviere immer Strict Mode in neuen Projekten – die Vorab-Kosten sind die Sicherheit wert.
{
"compilerOptions": {
// 'strict: true' enables ALL of these:
"strictNullChecks": true,
// null and undefined are NOT assignable to other types
// without explicit union. Forces null handling.
let x: string = null; // ERROR (without this, it's allowed)
"noImplicitAny": true,
// Parameters/variables can't be implicitly 'any'
function fn(x) { } // ERROR: x is implicitly any
"strictFunctionTypes": true,
// Function parameter types checked contravariantly
// (catches unsafe function assignments)
"strictBindCallApply": true,
// bind/call/apply are type-checked
"strictPropertyInitialization": true,
// Class properties must be initialized or declared with !
class User {
name: string; // ERROR: not initialized
name2!: string; // OK: definite assignment assertion
}
"noImplicitThis": true,
// 'this' must have a known type (no implicit any)
"alwaysStrict": true
// Emit "use strict" in every file
}
}Modul-Auflösungs-Strategien
moduleResolution kontrolliert, wie Import-Pfade aufgelöst werden. 'node' ist die klassische Strategie; 'bundler' (TS 5.0+) matcht moderne Bundler wie Vite und unterstützt package.json-Exports. 'nodenext' ist striktes ESM (erfordert Erweiterungen). paths lässt dich Import-Aliase erstellen (@/ → src/), die in deiner Bundler-Config gespiegelt werden müssen (z.B. Vites resolve.alias). baseUrl + paths ist der Standardweg, um tiefe relative Imports (../../../) zu vermeiden.
{
"compilerOptions": {
// How TS resolves import paths
"moduleResolution": "node", // classic Node.js resolution
// Looks for: file.ts, file/index.ts, node_modules/file
"moduleResolution": "bundler", // for Vite/webpack/esbuild (TS 5.0+)
// Matches how bundlers resolve: supports import maps,
// conditional exports, no file extension requirement
"moduleResolution": "nodenext", // Node.js ESM resolution (strict)
// Requires file extensions in imports: import "./foo.js"
// Path mapping (aliases)
"baseUrl": ".",
"paths": {
"@/*": ["src/*"], // import "@/components/Button"
"@utils/*": ["src/utils/*"],
"@components": ["src/components/index.ts"]
}
// rootDirs: virtual directories that map to the same location
"rootDirs": ["src", "generated"],
// imports between src/ and generated/ resolve as if same dir
}
}
// In your code:
import { Button } from "@/components/Button";
import { formatDate } from "@utils/date";
// These resolve to src/components/Button.ts and src/utils/date.tsProject References (Monorepos)
Project References teilen eine große Codebase in unabhängig kompilierte Sub-Projekte auf – essenziell für Monorepos. Jedes Projekt hat composite: true und emitiert Deklarationen. References deklarieren Abhängigkeiten zwischen Projekten. tsc --build (-b) kompiliert in Abhängigkeitsreihenfolge und baut nur neu, was sich geändert hat. Das beschleunigt Typ-Prüfung für große Codebasen drastisch und erzwingt architektonische Grenzen zwischen Paketen.
// tsconfig.json (root — references sub-projects)
{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true, // required for project references
"declaration": true, // must emit declarations
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
// packages/api/tsconfig.json
{
"compilerOptions": { /* ... */ },
"references": [
{ "path": "../shared" } // depends on shared
]
}
// Benefits:
// 1. Faster builds — only rebuild changed projects
// 2. Clear dependency boundaries between packages
// 3. Type-checking is scoped per project
// 4. tsc --build handles the build order automatically
// Build command: tsc --build (or tsc -b)Häufige tsconfig-Rezepte
Verschiedene Projekttypen brauchen unterschiedliche tsconfig-Einstellungen. React/Vite verwendet jsx: 'react-jsx' und noEmit (Vite kompiliert). Node.js verwendet CommonJS (oder NodeNext für ESM) und types: ['node']. Bibliotheken brauchen declaration: true für .d.ts-Ausgabe und ein niedrigeres target für breitere Kompatibilität. isolatedModules wird von Vite/esbuild benötigt (jede Datei muss unabhängig kompilierbar sein). Schließe immer Test-Dateien und node_modules vom Build aus.
// React + Vite project
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx", // React 17+ JSX transform
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"noEmit": true, // Vite handles compilation
"isolatedModules": true, // required by Vite/esbuild
"verbatimModuleSyntax": true
},
"include": ["src"]
}
// Node.js backend project
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS", // or NodeNext for ESM
"moduleResolution": "node",
"lib": ["ES2022"],
"types": ["node"], // @types/node
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}
// Library project (publishes to npm)
{
"compilerOptions": {
"target": "ES2020", // broader compatibility
"module": "ESNext",
"declaration": true, // emit .d.ts
"declarationMap": true,
"outDir": "./dist",
"strict": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}Decorators
Klassen-Decorator
Klassen-Decorators empfangen den Konstruktor und können eine modifizierte Klasse zurückgeben. Sie sind experimentell (erfordern experimentalDecorators: true). Häufig in NestJS und TypeORM.
function logged<T extends new (...args: any[]) => any>(con: T) {
return class extends con {
created = new Date().toISOString();
};
}
@logged
class Service { constructor(public name: string) {} }Methoden-Decorator
Methoden-Decorators empfangen (target, key, descriptor). Das Wrappen von descriptor.value ermöglicht Logging, Caching, Validierung. So funktionieren NestJS-Interceptors.
function log(target: any, key: string, desc: PropertyDescriptor) {
const orig = desc.value;
desc.value = function(...args: any[]) {
console.log(`Calling ${key}`, args);
return orig.apply(this, args);
};
}
class Calc { @log add(a: number, b: number) { return a + b; } }Property-Decorator
Property-Decorators empfangen (target, key). Die Verwendung von Object.defineProperty erstellt Getter/Setter für Validierung. Verwendet in class-validator für DTO-Validierung.
function required(target: any, key: string) {
let val: any;
Object.defineProperty(target, key, {
get() { return val; },
set(v: any) { if (v == null) throw new Error(`${key} required`); val = v; }
});
}Parameter-Decorator
Parameter-Decorators empfangen (target, methodKey, parameterIndex). Verwendet mit Metadata-Reflection für Validierung. class-validator und NestJS verwenden dies.
function Min(min: number) {
return (target: any, key: string, idx: number) => {
console.log(`${key} param ${idx} >= ${min}`);
};
}
class Order { create(@Min(0) qty: number) { return qty; } }Decorator-Factory
Decorator-Fabriken geben eine Decorator-Funktion zurück und ermöglichen Konfiguration. Die äußere Funktion empfängt Parameter, die innere ist der eigentliche Decorator.
function Retry(times: number) {
return function(target: any, key: string, desc: PropertyDescriptor) {
const orig = desc.value;
desc.value = async function(...args: any[]) {
for (let i = 0; i < times; i++)
try { return await orig.apply(this, args); }
catch (e) { if (i === times - 1) throw e; }
};
};
}Module-Augmentation
Eingebaute Typen augmentieren
Module-Augmentation erweitert existierende Typen. declare global erlaubt das Augmentieren eingebauter Typen wie Array. Die Runtime-Implementierung muss ebenfalls bereitgestellt werden.
declare global {
interface Array<T> {
last(): T | undefined;
chunk(size: number): T[][];
}
}
Array.prototype.last = function() { return this[this.length - 1]; };Bibliotheks-Typen augmentieren
Module-Augmentation erweitert Typen von Third-Party-Bibliotheken. declare module öffnet den Modul-Typ erneut. Essenziell, um benutzerdefinierte Eigenschaften zu Framework-Objekten hinzuzufügen.
declare module 'express' {
interface Request {
user?: { id: string; role: string };
}
}
app.get('/profile', (req, res) => {
const userId = req.user?.id; // Typed!
});Window augmentieren
Das Augmentieren von Window fügt benutzerdefinierte globale Eigenschaften mit Typsicherheit hinzu. Nützlich, um App-State für Debugging-Tools oder Analytics freizugeben.
declare global {
interface Window {
myApp: { init: () => void; version: string };
}
}
window.myApp = { init: () => console.log('Ready'), version: '1.0.0' };CSS-Module
CSS-Module brauchen Typdeklarationen. Die Deklaration mappt .module.css-Imports auf einen Record von Klassennamen. Ermöglicht Autovervollständigung für CSS-Klassenreferenzen.
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
import styles from './Button.module.css';
<button className={styles.button} />Vue-Plugin
Vue und andere Frameworks verwenden Module-Augmentation für Plugin-Typisierung. ComponentCustomProperties fügt Instanz-Eigenschaften hinzu. Ermöglicht typsichere Plugins.
declare module 'vue' {
interface ComponentCustomProperties {
$auth: { login: () => Promise<void> };
}
}
export default defineComponent({
methods: { async login() { await this.$auth.login(); } }
});Declaration Merging
Interfaces mergen
Interfaces mit demselben Namen werden automatisch gemerged. Alle Member werden Teil eines einzigen Interfaces. Nützlich, um Interfaces über Dateien hinweg zu teilen.
interface User { name: string; }
interface User { age: number; }
interface User { email: string; }
const user: User = { name: 'Alice', age: 30, email: '[email protected]' };Namespaces mergen
Namespaces mit demselben Namen mergen ihre Exports. Das erlaubt das Aufteilen von Namespace-Inhalten über Dateien hinweg. ES-Module werden für neuen Code bevorzugt.
namespace Utils {
export function log(msg: string) { console.log(msg); }
}
namespace Utils {
export function warn(msg: string) { console.warn(msg); }
}
Utils.log('info'); Utils.warn('alert');Namespace mit Funktion
Namespaces können mit Funktionen, Klassen und Enums gemerged werden. Der Namespace fügt der Funktion statische Eigenschaften hinzu. Verwendet in Moment.js und ähnlichen Bibliotheken.
function Counter() { Counter.count++; }
namespace Counter {
export let count = 0;
export function reset() { count = 0; }
}
Counter(); Counter();
console.log(Counter.count); // 2Mit Klassen mergen
Das Mergen eines Namespaces mit einer Klasse fügt statische Member und verschachtelte Typen hinzu. Der Namespace kann Interfaces exportieren, die zu verschachtelten Typen werden.
class Settings { static defaults = { theme: 'light' }; }
namespace Settings {
export interface Options { theme: string; lang: string; }
}
const opts: Settings.Options = { theme: 'dark', lang: 'en' };Unzulässige Merges
Klassen können nicht mit anderen Klassen gemerged werden. Variablen können nicht mergen. Funktionen mergen als Overloads. Enums können mit Namespaces mergen.
// Cannot merge classes
class A { x = 1; }
class A { y = 2; } // Error
// Function overloads (allowed)
function fn(x: string): string;
function fn(x: number): number;
function fn(x: any): any { return x; }Type-Narrowing
typeof & instanceof
typeof grenzt primitive Typen ein. instanceof grenzt Klassen-Typen ein. TypeScript versteht diese Prüfungen und grenzt den Typ in jedem Zweig ein.
function process(value: string | number | Date) {
if (typeof value === 'string') return value.toUpperCase();
if (typeof value === 'number') return value.toFixed(2);
if (value instanceof Date) return value.toISOString();
}in-Operator
Der in-Operator prüft, ob eine Eigenschaft existiert, und grenzt den Typ ein. Nützlich für discriminated unions mit unterschiedlichen Eigenschaftsnamen.
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function speak(animal: Cat | Dog) {
if ('meow' in animal) animal.meow();
else animal.bark();
}Discriminated Unions
Discriminated unions verwenden eine gemeinsame Eigenschaft (Diskriminans), um Typen einzugrenzen. Switche auf das Diskriminans für erschöpfende Prüfung. Sicherstes Pattern für Variant-Typen.
type Result =
| { status: 'success'; data: string }
| { status: 'error'; message: string };
function handle(r: Result) {
switch (r.status) {
case 'success': console.log(r.data); break;
case 'error': console.log(r.message); break;
}
}Type-Predicates
Type-Predicates (x is T) ermöglichen benutzerdefinierte Narrowing-Funktionen. Rückgabe true grenzt zu T ein, false grenzt zum ausgeschlossenen Typ ein. TypeScript vertraut dem Predicate blind.
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) pet.swim();
else pet.fly();
}Assertion-Funktionen
Assertion-Funktionen werfen, wenn die Bedingung fehlschlägt, und grenzen den Typ für nachfolgenden Code ein. asserts x is T grenzt zu T ein. Eliminiert redundante Null-Prüfungen.
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value == null) throw new Error('Null or undefined');
}
function processUser(user?: User) {
assertDefined(user);
console.log(user.name); // User (not undefined)
}Template-Literal-Typen
Basis-Template-Literale
Template-Literal-Typen erstellen String-Patterns. Sie beschränken Strings darauf, einem Template zu entsprechen. Ermöglicht typsichere Patterns für API-Endpunkte und Event-Namen.
type Greeting = `hello ${string}`;
const g: Greeting = 'hello world'; // OK
type Endpoint = `${'GET' | 'POST'} /api/${string}`;
const ep: Endpoint = 'GET /api/users';Uppercase & Lowercase
Eingebaute intrinsische Typen transformieren String-Literal-Typen. Kombiniere mit Template-Literalen, um typsichere Event-Namen und Konstanten zu generieren.
type Upper = Uppercase<'hello'>; // 'HELLO'
type Lower = Lowercase<'WORLD'>; // 'world'
type Cap = Capitalize<'foo'>; // 'Foo'
type EventName = `on${Capitalize<'click'>}`; // 'onClick'Key-Remapping
Key-Remapping (as-Klausel) transformiert Schlüssel während Mapped Types. Generiert Getter-/Setter-Namen aus Eigenschaftsnamen. Erstellt typsichere APIs aus Interfaces.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}>`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }String-Pattern-Matching
Template-Literal-Typen mit infer können Strings zur Compile-Zeit parsen. Split bricht einen String in ein Tuple. Ermöglicht typsichere String-Manipulation.
type Split<S extends string, D extends string> =
S extends `${infer L}${D}${infer R}` ? [L, ...Split<R, D>] : [S];
type Parts = Split<'a,b,c', ','>; // ['a', 'b', 'c']Event-System-Typisierung
Template-Literal-Typen mit Generics erstellen vollständig typsichere Event-Systeme. Der Event-Name bestimmt den Payload-Typ. on und emit erzwingen übereinstimmende Typen.
type EventMap = { click: { x: number }; submit: { value: string } };
class Emitter {
on<K extends keyof EventMap>(event: K, handler: (e: EventMap[K]) => void) {}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {}
}
em.on('click', e => console.log(e.x)); // Typed!infer-Schlüsselwort
Rückgabetyp extrahieren
infer deklariert eine Typvariable innerhalb eines Conditional Types. Es erfasst den Typ an einer spezifischen Position. ReturnType ist das eingebaute Äquivalent.
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { name: 'Alice', age: 30 }; }
type User = ReturnOf<typeof getUser>; // { name: string; age: number; }Promise-Typ extrahieren
infer extrahiert den inneren Typ eines Promises. DeepUnwrap entpackt verschachtelte Promises rekursiv. Das eingebaute Awaited<T> macht dies in modernem TypeScript.
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type R = UnwrapPromise<Promise<string>>; // string
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type D = DeepUnwrap<Promise<Promise<boolean>>>; // booleanArray-Element extrahieren
infer E erfasst den Elementtyp eines Arrays. Für Tuples kann infer spezifische Positionen erfassen. Nützlich für die Arbeit mit generischen Collections.
type ElementOf<T> = T extends (infer E)[] ? E : never;
type T1 = ElementOf<string[]>; // string
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[string, number]>; // stringFunktionsparameter extrahieren
infer P erfasst das Parameter-Tuple einer Funktion. Parameters ist das eingebaute Äquivalent. Nützlich für das Wrappen von Funktionen unter Bewahrung der Typen.
type Params<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) { return ''; }
type P = Params<typeof greet>; // [string, number]Mehrfaches infer
Mehrere infer-Variablen können verschiedene Teile eines Typs gleichzeitig erfassen. Ermöglicht komplexe Typ-Transformationen in einem einzigen Conditional.
type FirstLast<T extends any[]> =
T extends [infer First, ...any[], infer Last]
? { first: First; last: Last } : never;
type R = FirstLast<[1, 2, 3, 4]>; // { first: 1; last: 4 }Varianz
Kovarianz
Kovarianz erlaubt, dass Dog[] Animal[] zugewiesen wird. TypeScript-Arrays sind kovariant, aber das ist unsound: Ein Animal in ein Dog[] zu pushen korrumpiert das Array.
class Animal { name: string; }
class Dog extends Animal { breed: string; }
let dogs: Dog[] = [new Dog()];
let animals: Animal[] = dogs; // OK (covariant)
// animals.push(new Animal()); // Unsafe at runtimeKontravarianz
Kontravarianz bedeutet, dass eine Funktion, die Dog akzeptiert, dort verwendet werden kann, wo eine Funktion, die Animal akzeptiert, erwartet wird. Sicher, weil ein Dog-Handler jedes Animal behandelt, das ein Dog ist.
type Handler<T> = (arg: T) => void;
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);
let animalHandler: Handler<Animal> = dogHandler; // OK with strictFunctionTypesBivarianz
Methoden-Syntax ist bivariant. Function-Property-Syntax ist kontravariant mit strictFunctionTypes. Methoden sind bivariant für OO-Kompatibilität.
interface IFace {
method(x: Animal): void; // bivariant
fn: (x: Animal) => void; // contravariant
}
class Impl implements IFace {
method(x: Dog) {} // OK (bivariant)
fn = (x: Dog) => {} // Error (contravariant)
}in/out-Varianz
TypeScript 4.7+ unterstützt explizite Varianz-Annotationen. in markiert kontravariant (Consumer), out markiert kovariant (Producer), in out markiert invariant (beides).
interface Producer<out T> { produce(): T; }
interface Consumer<in T> { consume(value: T): void; }
interface Buffer<in out T> { read(): T; write(value: T): void; }Invariante Typen
Invariante Typen erfordern exakte Typ-Übereinstimmungen. Ein Typ ist invariant, wenn er sowohl in Eingabe- als auch Ausgabe-Positionen erscheint. Box<Dog> kann Box<Animal> nicht zugewiesen werden.
interface Box<T> { get(): T; set(value: T): void; }
let dogBox: Box<Dog> = {} as any;
let animalBox: Box<Animal> = dogBox; // Error: invariantBuilder-Pattern
Fluent Builder
Das Builder-Pattern konstruiert komplexe Objekte Schritt für Schritt. Jede Methode gibt this zurück für Chaining. Nützlich für SQL-Queries, HTTP-Anfragen und Konfiguration.
class QueryBuilder {
private parts: string[] = [];
select(cols: string): this { this.parts.push(`SELECT ${cols}`); return this; }
from(table: string): this { this.parts.push(`FROM ${table}`); return this; }
where(cond: string): this { this.parts.push(`WHERE ${cond}`); return this; }
build(): string { return this.parts.join(' '); }
}Typsicherer Builder
Typsichere Builder verwenden Conditional Types, um erforderliche Felder zu erzwingen. build() gibt nur Person zurück, wenn hasName true ist. Fängt fehlende Felder zur Compile-Zeit ab.
interface State { hasName: boolean; }
class Builder<S extends State> {
name(n: string): Builder<S & { hasName: true }> { return this as any; }
build(): S extends { hasName: true } ? Person : never { return {} as any; }
}Unveränderlicher Builder
Unveränderliche Builder erstellen eine neue Instanz für jede Modifikation. Das Typsystem verfolgt alle hinzugefügten Schlüssel durch Intersection-Typen. Jedes set gibt einen neuen Builder-Typ zurück.
class ImmutableBuilder<T extends object> {
constructor(private data: T) {}
set<K extends string, V>(key: K, value: V):
ImmutableBuilder<T & { [P in K]: V }> {
return new ImmutableBuilder({ ...this.data, [key]: value });
}
build(): T { return this.data; }
}Director-Pattern
Der Director kapselt häufige Konstruktions-Sequenzen. Er verwendet einen Builder, um Standard-Produkte zu erstellen. Verschiedene Directors produzieren unterschiedliche Variationen.
class HTMLBuilder {
private html = '';
addTag(tag: string, content: string): this {
this.html += `<${tag}>${content}</${tag}>`; return this;
}
build(): string { return this.html; }
}
class Director {
buildPage(title: string, body: string): string {
return new HTMLBuilder().addTag('title', title).addTag('body', body).build();
}
}Step-Builder
Step-Builder erzwingt eine spezifische Reihenfolge von Methodenaufrufen durch das Typsystem. Jeder Schritt gibt einen anderen Typ zurück mit nur der nächsten verfügbaren Methode.
class Step1 { name(n: string): Step2 { return new Step2(n); } }
class Step2 {
constructor(private name: string) {}
age(a: number): Step3 { return new Step3(this.name, a); }
}
class Step3 {
constructor(private name: string, private age: number) {}
build() { return { name: this.name, age: this.age }; }
}Testing mit TypeScript
Jest mit TypeScript
Verwende ts-jest oder @swc/jest für TypeScript-Tests. describe gruppiert verwandte Tests, it definiert Testfälle. expect erstellt Assertions mit Matchern wie toBe, toEqual.
import { sum } from './sum';
describe('sum', () => {
it('adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
it('handles negatives', () => {
expect(sum(-1, -2)).toBe(-3);
});
});Typ-Testing
expectTypeOf testet Typen zur Compile-Zeit. Verifiziert Rückgabetypen, Parametertypen und aufgelöste Promise-Typen. Fehlschlagen des Builds, wenn Typen falsch sind.
import { expectTypeOf } from 'vitest';
type GetUser = (id: string) => Promise<{ name: string }>;
test('return type is correct', () => {
const fn = {} as unknown as GetUser;
expectTypeOf(fn).returns.toEqualTypeOf<{ name: string }>();
expectTypeOf(fn).parameters.toEqualTypeOf<[string]>();
});Mocking mit Typen
jest.Mocked<T> erstellt einen typisierten Mock aus einem Interface. jest.fn() erstellt Mock-Funktionen mit typisierten Rückgabewerten. Der Mock ist vollständig typisiert.
interface Database { find(id: string): Promise<User>; }
const mockDb: jest.Mocked<Database> = {
find: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
mockDb.find.mockResolvedValueOnce({ id: '2', name: 'Bob' });Test-Setup & Teardown
beforeAll läuft einmal vor allen Tests, afterAll einmal danach. beforeEach läuft vor jedem Test, afterEach nach jedem. Verwende für Setup und Cleanup.
describe('Database', () => {
beforeAll(async () => { db = createDatabase(); await db.connect(); });
afterAll(async () => { await db.disconnect(); });
beforeEach(async () => { await db.clear(); });
afterEach(() => { jest.restoreAllMocks(); });
});Property-Based-Testing
Property-Based-Testing generiert zufällige Eingaben, um Invarianten zu testen. fc.assert führt die Property mehrmals aus. Fängt Edge-Cases, die beispielbasierte Tests verpassen.
import { fc } from 'fast-check';
describe('sort', () => {
it('preserves length', () => {
fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
return [...arr].sort().length === arr.length;
}));
});
});Häufige Fallstricke
any vs unknown
any deaktiviert Typprüfungen und verbirgt Bugs. unknown ist typsicher: Du musst es vor der Verwendung eingrenzen. Verwende unknown für nicht vertrauenswürdige Quellen (API, JSON.parse).
// BAD: any disables type checking
function bad(data: any) { return data.foo.bar; }
// GOOD: unknown forces type checking
function good(data: unknown) {
if (typeof data === 'object' && data !== null && 'foo' in data)
return (data as any).foo;
}Exzessive Eigenschafts-Prüfungen
TypeScript prüft exzessive Eigenschaften nur bei Objekt-Literalen, die direkt zugewiesen werden. Über eine Variable wird die Prüfung übersprungen. Verwende zod für strengere Runtime-Validierung.
interface User { name: string; age: number; }
// Direct literal: checked
const u1: User = { name: 'A', age: 30, extra: true }; // Error
// Via variable: not checked
const data = { name: 'A', age: 30, extra: true };
const u2: User = data; // OKEnum vs Union
Enums erstellen Runtime-Objekte mit Reverse-Mapping. Union-Typen sind Zero-Runtime und tree-shakeable. Bevorzuge Union-Typen für neuen Code.
// Enum: runtime object
enum Color { Red, Green, Blue }
// Union: no runtime
type Color2 = 'red' | 'green' | 'blue';
// Const enum: erased
const enum Dir { Up, Down }Strukturelle Typisierung
TypeScript verwendet strukturelle Typisierung: Typen sind kompatibel, wenn Formen übereinstimmen. Admin ist User zuweisbar. Das kann logische Bugs verursachen. Branded-Typen fügen nominale Unterscheidung hinzu.
interface User { name: string; age: number; }
interface Admin { name: string; age: number; role: string; }
const admin: Admin = { name: 'Bob', age: 40, role: 'admin' };
const user: User = admin; // OK (structural)Type-Assertion-Gefahren
Type-Assertions (as) überschreiben TypeScript ohne Runtime-Prüfungen. Verwende Runtime-Validierung (zod, io-ts) für externe Daten. safeParse gibt ein Result zurück ohne zu werfen.
// BAD: assertion hides errors
const user = JSON.parse(input) as User;
// GOOD: runtime validation
import { z } from 'zod';
const schema = z.object({ name: z.string() });
const result = schema.safeParse(JSON.parse(input));
if (result.success) { const user: User = result.data; }Verwandte TypeScript-Snippets
Copy-paste ready code for common tasks.
Generische Funktionen
Generische Funktionen definieren und verwenden.
Bedingte Typen
Typen basierend auf Bedingungen auswählen.
Mapped Types
Neue Typen aus bestehenden konstruieren.
Utility Types
TypeScript-eingebaute Utility-Typen.
Type Guards
Benutzerdefinierte Type-Guard-Funktionen.
Funktions-Overloads
Funktions-Overload-Signaturen definieren.
Decorators
Klassen- und Methoden-Decorators.
Enum
Numerische, String- und const-Enums.
Interface-Vererbung
Interface-Vererbung und -Implementierung.
Abstrakte Klassen
Abstrakte Klassen und abstrakte Methoden definieren.
Namespaces
Code mit Namespaces organisieren.
Modul-Deklarationen
Typ-Deklarationen für JS-Bibliotheken schreiben.
Deklarations-Zusammenführung
Mehrere Deklarationen mit demselben Namen zusammenführen.
Optional Chaining
Sicher auf tiefe Eigenschaften zugreifen.
Nullish Coalescing
Einen Standardwert nur für null/undefined verwenden.
Typ-Inferenz
TypeScript schließt Typen automatisch ein.
const Assertions
Typen mit as const eingrenzen.
satisfies-Operator
Typ-Prüfung unter Beibehaltung des engsten Typs.
infer-Schlüsselwort
Typen innerhalb bedingter Typen extrahieren.
Template Literal Types
Typen basierend auf Strings konstruieren.
Was this helpful?