Funktionale Programmiermuster in TypeScript
Pragmatische funktionale Programmiermuster für TypeScript, die die Code-Klarheit verbessern, ohne einen vollständigen Paradigmenwechsel zu erfordern.

Funktionale Programmierung in TypeScript bedeutet nicht, Klassen aufzugeben, alles mit Monaden neu zu schreiben oder eine puristische Philosophie zu übernehmen. Sie bedeutet, gezielt Muster anzuwenden — Unveränderlichkeit, reine Funktionen, Komposition — wo sie Komplexität reduzieren und Testbarkeit verbessern. Diese Muster koexistieren natürlich mit objektorientiertem Code. Das Ziel ist Pragmatismus, nicht Ideologie.
Die Muster, die im alltäglichen TypeScript den größten Wert liefern, sind einfacher, als du denkst.
Reine Funktionen
Eine reine Funktion liefert bei gleicher Eingabe die gleiche Ausgabe und hat keine Seiteneffekte. Diese eine Eigenschaft macht eine Funktion trivial testbar, sicher parallelisierbar und leicht nachvollziehbar.
// ❌ Impure — depends on external state, mutates input
let taxRate = 0.08;
function calculateTotal(items: CartItem[]) {
let total = 0;
items.forEach(item => {
item.totalPrice = item.price * item.quantity; // mutation
total += item.totalPrice;
});
return total * (1 + taxRate); // depends on external variable
}
// ✅ Pure — all inputs explicit, no mutation, no side effects
function calculateTotal(items: readonly CartItem[], taxRate: number): number {
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return subtotal * (1 + taxRate);
}Nicht jede Funktion kann rein sein — HTTP-Requests, Datenbankaufrufe und Logging sind inhärent unrein. Die Strategie ist, unreine Operationen an die Ränder des Systems zu schieben und die Kernlogik rein zu halten.
Unveränderlichkeit als Standard
Mutation ist die Ursache einer ganzen Klasse von Bugs: veraltete Closures, korrupte Shared State, React-Rendering-Probleme, Race Conditions. TypeScript bietet readonly, um Mutation auf Typebene zu verhindern.
// ❌ Mutation — callers don't expect their data to change
function addDiscount(order: Order): Order {
order.total *= 0.9; // mutates the original
order.discountApplied = true;
return order;
}
// ✅ Immutable — returns a new object
function addDiscount(order: Readonly<Order>): Order {
return {
...order,
total: order.total * 0.9,
discountApplied: true,
};
}Verwende readonly in Funktionsparametern, um Unveränderlichkeit durchzusetzen:
// ReadonlyArray prevents push, pop, splice
function getTopScores(scores: readonly number[], limit: number): number[] {
// scores.sort() would error — readonly
return [...scores].sort((a, b) => b - a).slice(0, limit);
}
// Readonly<T> makes all properties readonly
interface Config {
host: string;
port: number;
features: string[];
}
function createServer(config: Readonly<Config>): Server {
// config.port = 8080; // Error: readonly
return new Server({ ...config });
}Funktionskomposition
Komposition baut komplexe Operationen aus einfachen, wiederverwendbaren Funktionen. Statt einer einzigen Funktion, die fünf Dinge tut, komponierst du fünf Funktionen, die jeweils eine Sache tun.
// Pipe: left-to-right composition
function pipe<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
return (arg: T) => fns.reduce((result, fn) => fn(result), arg);
}
// Individual transformation functions
const trim = (s: string) => s.trim();
const lowercase = (s: string) => s.toLowerCase();
const slugify = (s: string) => s.replace(/\s+/g, '-');
const removeSpecialChars = (s: string) => s.replace(/[^a-z0-9-]/g, '');
// Compose them into a pipeline
const toSlug = pipe(trim, lowercase, slugify, removeSpecialChars);
toSlug(" Hello World! 123 "); // "hello-world-123"Für heterogene Typen unterstützt TypeScript variadische Generics in einem generischen pipe nicht gut. Verwende explizite Ketten oder Bibliotheken:
// Array processing pipeline
function processUsers(users: User[]): UserSummary[] {
return users
.filter(isActive)
.filter(hasVerifiedEmail)
.map(toUserSummary)
.sort(byLastLogin);
}
// Each function is pure, testable independently
const isActive = (user: User): boolean => user.status === 'active';
const hasVerifiedEmail = (user: User): boolean => user.emailVerified;
const toUserSummary = (user: User): UserSummary => ({
id: user.id,
name: user.name,
lastLogin: user.lastLoginAt,
});
const byLastLogin = (a: UserSummary, b: UserSummary): number =>
b.lastLogin.getTime() - a.lastLogin.getTime();Option/Result-Typen für Fehlerbehandlung
Anstatt bei erwarteten Fehlern Exceptions zu werfen, verwende Result-Typen, die Aufrufer zwingen, sowohl Erfolgs- als auch Fehlerfälle zu behandeln.
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// Usage — caller must handle both cases
function parseConfig(raw: string): Result<Config, string> {
try {
const parsed = JSON.parse(raw);
if (!parsed.host || !parsed.port) {
return err('Missing required fields: host, port');
}
return ok({ host: parsed.host, port: parsed.port });
} catch {
return err('Invalid JSON');
}
}
const result = parseConfig(configString);
if (result.ok) {
startServer(result.value); // TypeScript knows value exists
} else {
console.error(result.error); // TypeScript knows error exists
}// ❌ Exception-based — nothing forces callers to handle errors
function findUser(id: string): User {
const user = db.find(id);
if (!user) throw new NotFoundError('User not found');
return user;
}
// Caller might forget try/catch — runtime explosion
// ✅ Result-based — type system enforces error handling
function findUser(id: string): Result<User, 'not-found'> {
const user = db.find(id);
if (!user) return err('not-found');
return ok(user);
}
// Caller can't access .value without checking .ok firstFunktionen höherer Ordnung
Funktionen, die Funktionen als Argumente erhalten oder Funktionen zurückgeben. Du verwendest sie bereits — map, filter, reduce sind Funktionen höherer Ordnung.
// Factory function — returns configured function
function createValidator<T>(rules: Array<(value: T) => string | null>) {
return (value: T): string[] => {
return rules
.map(rule => rule(value))
.filter((error): error is string => error !== null);
};
}
const validatePassword = createValidator<string>([
(pw) => pw.length < 8 ? 'Must be at least 8 characters' : null,
(pw) => !/[A-Z]/.test(pw) ? 'Must contain uppercase letter' : null,
(pw) => !/[0-9]/.test(pw) ? 'Must contain a number' : null,
]);
validatePassword('weak'); // ['Must be at least 8 characters', 'Must contain uppercase letter', 'Must contain a number']
validatePassword('StrongPass1'); // []Memoization
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}
// Expensive computation — memoize it
const computeLayout = memoize((width: number, items: number): Layout => {
// ... complex layout calculation
return { columns: Math.floor(width / 300), rows: Math.ceil(items / 3) };
});Memoization funktioniert nur für reine Funktionen. Wenn die Funktion von externem Zustand abhängt, kann das gecachte Ergebnis veraltet sein.
Praktisches funktionales Refactoring
Diese Muster brillieren beim Refactoring komplexen prozeduralen Codes. Der Vorher/Nachher-Unterschied ist dramatisch.
// ❌ Procedural — hard to test, hard to modify
async function processOrders(orders: Order[]): Report {
const report: Report = { total: 0, byStatus: {}, errors: [] };
for (const order of orders) {
try {
if (!order.items?.length) {
report.errors.push(`Order ${order.id}: no items`);
continue;
}
const total = order.items.reduce((s, i) => s + i.price * i.qty, 0);
const tax = total * 0.08;
const status = total > 1000 ? 'high-value' : 'standard';
report.total += total + tax;
report.byStatus[status] = (report.byStatus[status] ?? 0) + 1;
} catch (e) {
report.errors.push(`Order ${order.id}: ${e}`);
}
}
return report;
}
// ✅ Functional — each step is independently testable
const calculateOrderTotal = (order: Order): number =>
order.items.reduce((sum, item) => sum + item.price * item.qty, 0);
const applyTax = (rate: number) => (amount: number): number =>
amount * (1 + rate);
const classifyOrder = (total: number): string =>
total > 1000 ? 'high-value' : 'standard';
const validateOrder = (order: Order): Result<Order, string> =>
order.items?.length ? ok(order) : err(`Order ${order.id}: no items`);
function processOrders(orders: Order[]): Report {
const results = orders.map(order => {
const validation = validateOrder(order);
if (!validation.ok) return { error: validation.error };
const total = applyTax(0.08)(calculateOrderTotal(order));
return { total, status: classifyOrder(total) };
});
return {
total: results.reduce((s, r) => s + ('total' in r ? r.total : 0), 0),
byStatus: results.reduce((acc, r) => {
if ('status' in r) acc[r.status] = (acc[r.status] ?? 0) + 1;
return acc;
}, {} as Record<string, number>),
errors: results.filter(r => 'error' in r).map(r => (r as any).error),
};
}Wichtige Erkenntnisse
- Reine Funktionen sind das Muster mit dem größten Hebel — keine Seiteneffekte bedeuten trivialen Test und sicheres Refactoring
- Standardmäßig Unveränderlichkeit — verwende
readonlyund Spread-Operatoren. Mutation ist eine Optimierung, keine Standardeinstellung. - Komposition statt Komplexität — baue Pipelines aus kleinen Funktionen statt monolithischer Prozeduren
- Result-Typen ersetzen Exceptions bei erwarteten Fehlern — das Typsystem erzwingt Fehlerbehandlung
- Funktionen höherer Ordnung reduzieren Duplikation — Factories und Decorators erzeugen konfiguriertes Verhalten ohne Wiederholung
- Schrittweise anwenden — du musst nicht alles neu schreiben. Starte mit reinen Utility-Funktionen und komponiere von dort aus


