Was passiert, wenn dieser Code ausgeführt wird?
let x: number = 5;
let y: string = x;TypeScript erlaubt es nicht, einen number-Wert einer string-Variable zuzuweisen. Das ist ein Compile-Time-Error, den das Typsystem erkennt.
Alle Fragen mit den richtigen Antworten und Erklärungen.
let x: number = 5;
let y: string = x;TypeScript erlaubt es nicht, einen number-Wert einer string-Variable zuzuweisen. Das ist ein Compile-Time-Error, den das Typsystem erkennt.
? bei age?interface User {
name: string;
age?: number;
}
const user: User = { name: "Alice" };Das ? nach einer Property in einem Interface markiert sie als optional. Das Objekt muss sie nicht enthalten.
type Status = "active" | "inactive" | "pending";
const s: Status = "deleted";Ein Union Type schränkt die erlaubten Werte ein. 'deleted' ist nicht Teil des Status-Typs, daher meldet der Compiler einen Fehler.
identity<string>(42)?function identity<T>(arg: T): T {
return arg;
}
const result = identity<string>(42);Der Generic T wurde explizit auf string gesetzt, aber das Argument 42 ist vom Typ number. TypeScript meldet einen Compile-Error.
console.log(Direction.Up) ausgeben?enum Direction {
Up,
Down,
Left,
Right
}
console.log(Direction.Up);In einem numerischen Enum ohne explizite Werte hat das erste Element den Wert 0, die folgenden zählen um 1 hoch.
push aufzurufen?const arr: readonly number[] = [1, 2, 3];
arr.push(4);Ein readonly Array hat keine mutierenden Methoden wie push, pop, splice. Das ist Schutz auf Typebene.
&-Operator im Typkontext?type Point = {
x: number;
y: number;
};
type Point3D = Point & { z: number };
const p: Point3D = { x: 1, y: 2, z: 3 };Der &-Operator erzeugt einen Intersection Type, der alle Properties beider Typen zu einem zusammenführt.
void?function greet(name: string): void {
console.log(`Hello, ${name}`);
}void bedeutet, dass die Funktion keinen Wert zurückgibt. Unterscheidet sich von never, was heißt, dass die Funktion nie terminiert.
as const in diesem Kontext?const obj = { name: "Alice", age: 30 } as const;as const erzeugt eine Const Assertion — alle Properties werden readonly mit Literal-Typen (z. B. 'Alice' statt string).
type StringOrNumber = string | number;
function process(val: StringOrNumber) {
if (typeof val === "string") {
return val.toUpperCase();
}
return val.toFixed(2);
}Type Narrowing ist die Technik, den Typ innerhalb eines Conditional-Blocks einzuschränken. TypeScript erkennt den Typ nach einem typeof-Check automatisch.
Config.interface Config {
debug: boolean;
}
interface Config {
verbose: boolean;
}
const cfg: Config = { debug: true, verbose: false };Declaration Merging ist eine besondere Eigenschaft von Interfaces — mehrere Deklarationen mit gleichem Namen werden zu einem Typ vereint. Type Aliases (type) können das nicht.
function handleInput(val: unknown) {
return val.toUpperCase();
}Der Typ unknown ist die sichere Alternative zu any. Vor der Nutzung musst du den Wert einengen, z. B. mit typeof val === 'string'. Wichtiger Unterschied: any deaktiviert die Prüfung, unknown erzwingt sie.
val is string im Rückgabetyp?function isString(val: unknown): val is string {
return typeof val === "string";
}
function process(input: unknown) {
if (isString(input)) {
console.log(input.toUpperCase());
}
}Ein Type Predicate (val is string) ist ein Custom Type Guard. Wenn die Funktion true zurückgibt, engt TypeScript den Typ im Conditional-Block automatisch ein. Stärker als ein normales boolean-Return.
let a: Object = "hello";
let b: object = "hello";
let c: {} = "hello";Object (großes O) und {} akzeptieren Primitives (string, number). object (klein) akzeptiert nur nicht-primitive Typen (Objekte, Arrays, Funktionen). String ist ein Primitive, daher wirft b einen Fehler.
new Animal()?abstract class Animal {
abstract speak(): string;
greet(): string {
return `I say: ${this.speak()}`;
}
}
class Dog extends Animal {
speak() { return "Woof!"; }
}
const a = new Animal();Eine abstract class ist eine Basisklasse, die nicht instanziiert werden kann. Sie zwingt Subklassen, abstrakte Methoden zu implementieren. new Dog() funktioniert, new Animal() nicht.
class User {
constructor(
public name: string,
private password: string,
protected role: string
) {}
}
const u = new User("Alice", "secret", "admin");
console.log(u.name, u.password, u.role);public — überall zugänglich. private — nur innerhalb der Klasse. protected — innerhalb der Klasse und ihrer Subklassen. Von außen ist nur name sichtbar.
const user = {
name: "Alice",
address: {
city: null as string | null,
},
};
const city = user.address?.city ?? "Unknown";
console.log(city);?. (Optional Chaining) greift sicher auf verschachtelte Felder zu. ?? (Nullish Coalescing) gibt nur dann die rechte Seite zurück, wenn die linke null/undefined ist. city ist null, also ist das Ergebnis 'Unknown'.
!-Operator nach input und ist er sicher?function getLength(input: string | null): number {
return input!.length;
}Das !. (Non-Null Assertion) sagt dem Compiler: 'Ich weiß, das ist nicht null'. Es erzeugt keinen Runtime-Code — wenn der Wert tatsächlich null ist, bekommst du einen Runtime-Error. Type Guards sind sicherer.
Pair und warum kompiliert wrong nicht?type Pair = [string, number];
const p: Pair = ["age", 30];
const [label, value] = p;
const wrong: Pair = [30, "age"];Ein Tuple ist ein Array fester Länge mit konkreten Typen an jeder Position. [string, number] erwartet einen String an Index 0 und einen number an Index 1. Vertauschte Reihenfolge ist ein Fehler.
type und interface beim Erweitern von Typen?interface User {
name: string;
}
type Admin = User & { role: string };
// vs
interface Manager extends User {
department: string;
}Interface erweitert man mit extends und unterstützt Declaration Merging (mehrere Deklarationen). Type nutzt & (Intersection) und kann Unions, Tuples und Primitives. Für öffentliche APIs wird interface empfohlen.
type Person = {
name: string
age: number
}
type Employee = {
name: string
email: string
}
type A = Person | Employee;
type B = Person & Employee;
const a: A = {
name: 'John',
age: 20,
email: 'john@test-email.co.uk',
}
const b: B = {
name: 'John',
age: 20,
email: 'john@test-email.co.uk',
}
console.log('A:', a.name, a.age, a.email)
console.log('B:', b.name, b.age, b.email)Eine echte Falle! Obwohl Objekt a alle Felder hat, bedeutet der Typ A = Person | Employee 'entweder Person oder Employee'. TypeScript weiß nicht, welche Variante du hast — es könnte Person (ohne email) oder Employee (ohne age) sein. Deshalb werfen a.age und a.email ohne Narrowing einen Fehler. B = Person & Employee verlangt dagegen ALLE Felder beider Typen, sodass b.name, b.age und b.email garantiert sind.
val im letzten return?function formatValue(val: string | number | boolean) {
if (typeof val === "string") {
return val.toUpperCase();
}
if (typeof val === "number") {
return val.toFixed(2);
}
// Co wie TypeScript o val tutaj?
return val;
}TypeScript engt den Typ nach jedem Type Guard schrittweise ein. Nach typeof === 'string' fällt string weg, nach typeof === 'number' fällt number weg. Übrig bleibt boolean. Das ist automatisches Type Narrowing — der Compiler ermittelt es selbst.
res.data im success-Block und res.message im error-Block?type ApiResponse<T> =
| { status: "success"; data: T; timestamp: number }
| { status: "error"; message: string; code: number }
| { status: "loading" };
function handleResponse(res: ApiResponse<string[]>) {
if (res.status === "success") {
console.log(res.data.join(", "));
} else if (res.status === "error") {
console.log(res.message);
}
}status als Tag erlaubt TypeScript, den Typ in jedem Branch einzuengenEine Discriminated Union nutzt ein gemeinsames Feld (status), damit TypeScript den Typ automatisch einengt. Im Block status === 'success' weiß der Compiler, dass du die Variante mit data und timestamp hast. In error — message und code. Ganz ohne Casts.
ConfigKey und ConfigValue?const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: false,
} as const;
type Config = typeof config;
type ConfigKey = keyof Config;
type ConfigValue = Config[ConfigKey];Dank as const behalten die Werte ihre Literal-Typen (nicht string/number/boolean). typeof config extrahiert den Typ des Objekts. keyof liefert eine Union der Keys. Config[ConfigKey] ist ein Indexed Access Type — die Union ALLER Werttypen. Eine starke Kombination für typsichere Konfiguration.
in-Operator als Type Guard?interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly();
} else {
animal.swim();
}
}if-Block weiß TypeScript, dass wir Bird haben'fly' in animal prüft zur Laufzeit, ob das Objekt die Methode fly hat. TypeScript erkennt das als Type Guard: im if-Block weiß es, dass wir Bird haben (nur Bird hat fly). Im else weiß es, dass wir Fish haben. Der in-Operator ist eine Alternative zu instanceof, wenn du mit Interfaces arbeitest.
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // ?
type B = IsString<number>; // ?Conditional Types funktionieren wie ein Ternary: T extends string prüft, ob T string erweitert. Für string → 'yes', für number → 'no'.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};DeepReadonly läuft rekursiv durch alle Properties. Ist eine Property ein Objekt, wird der Typ erneut angewendet. Ergebnis: das gesamte Objekt wird unveränderlich.
Result?type ExtractReturn<T> = T extends (...args: any[]) => infer R
? R
: never;
type Result = ExtractReturn<() => Promise<string>>;Das Schlüsselwort infer R extrahiert den Rückgabetyp der Funktion. Die Funktion gibt Promise<string> zurück, also R = Promise<string>.
ClickEvent?type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">;Template Literal Types erlauben es, neue String-Typen zu bauen. Capitalize macht den ersten Buchstaben groß: 'click' → 'Click', also 'onClick'.
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>;
type B = Flatten<number>;Flatten extrahiert mit infer den Elementtyp eines Arrays. string[] → string. Für number (kein Array) wird der Typ selbst zurückgegeben.
Result?type Exclude<T, U> = T extends U ? never : T;
type Result = Exclude<"a" | "b" | "c", "a" | "c">;Exclude arbeitet distributiv über Union Types. Für jeden Member: erweitert er U → never (entfernt), sonst bleibt er. Übrig bleibt nur 'b'.
UserGetters?type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;Das Key Remapping (as) in Mapped Types erlaubt das Umbenennen von Keys. Jede Key K wird zu get${Capitalize<K>} transformiert, der Wert wird zu einer Getter-Funktion.
assertNever?function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
type Shape = "circle" | "square";
function area(shape: Shape) {
switch (shape) {
case "circle": return Math.PI;
case "square": return 1;
default: return assertNever(shape);
}
}assertNever akzeptiert never — einen Typ, den es eigentlich nie geben dürfte. Fügst du eine neue Variante zu Shape hinzu und behandelst sie nicht im switch, meldet der Compiler einen Fehler.
Result?type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends
(x: infer I) => void ? I : never;
type Result = UnionToIntersection<{ a: 1 } | { b: 2 }>;UnionToIntersection wandelt eine Union in eine Intersection um, indem es die Kontravarianz der Funktionsparameter-Position ausnutzt. Ergebnis: { a: 1 } & { b: 2 }.
payUSD(euros)?declare const brand: unique symbol;
type Branded<T, B> = T & { [brand]: B };
type USD = Branded<number, "USD">;
type EUR = Branded<number, "EUR">;
function payUSD(amount: USD) { /* ... */ }
const euros = 100 as EUR;
payUSD(euros);Branded Types (nominale Typen) fügen einem strukturellen Typ ein 'Phantom Brand' hinzu. USD und EUR sind unterschiedliche Typen, obwohl beide auf number basieren.
-? und -readonly in Mapped Types?type MakeRequired<T> = {
[P in keyof T]-?: T[P];
};
type MakeMutable<T> = {
-readonly [P in keyof T]: T[P];
};
interface Config {
readonly host?: string;
readonly port?: number;
}
type WritableConfig = MakeMutable<MakeRequired<Config>>;Das --Präfix in Mapped Types entfernt einen Modifier: -? macht ein Feld required (wie Required<T>), -readonly macht es veränderbar. WritableConfig wird zu { host: string; port: number }.
UserPreview und UserWithoutEmail?interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutEmail = Omit<User, "email">;Pick<T, K> wählt die angegebenen Keys aus einem Typ aus. Omit<T, K> entfernt die angegebenen Keys. Beide erzeugen einen neuen Typ mit einer Teilmenge der ursprünglichen Properties.
Record<Roles, boolean> und was würde passieren, wenn wir den Key viewer weglassen?type Roles = "admin" | "editor" | "viewer";
type Permissions = Record<Roles, boolean>;
const perms: Permissions = {
admin: true,
editor: true,
viewer: false,
};Record<K, V> erzeugt einen Typ mit Keys K und Werten V. Alle Keys sind verpflichtend — fehlt einer, gibt es einen Compile-Error.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rect":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}kind erlaubt TypeScript, den Typ in jedem Branch einzuengenEine Discriminated Union nutzt ein Tag-Feld (Discriminant) wie kind. TypeScript engt den Typ nach der Prüfung des Tags in switch/if automatisch ein — und gibt dir Zugriff auf die varianten-spezifischen Felder.
function logLength<T extends { length: number }>(obj: T): void {
console.log(obj.length);
}
logLength("hello");
logLength([1, 2, 3]);
logLength(42);extends { length: number } verlangt sieDer Generic Constraint T extends { length: number } fordert, dass T eine length-Property hat. String und Array haben length, also klappen sie. Number nicht — Compile-Error.
Result?function getParams<T extends (...args: any[]) => any>(
fn: T
): Parameters<T> {
return [] as any;
}
type Result = Parameters<(a: string, b: number) => void>;Parameters<T> extrahiert die Parametertypen einer Funktion als Tuple. Für (a: string, b: number) => void liefert es [string, number].
Result?type NonStringKeys<T> = {
[K in keyof T]: T[K] extends string ? never : K;
}[keyof T];
interface User {
id: number;
name: string;
age: number;
email: string;
}
type Result = NonStringKeys<User>;Dieser Typ filtert Keys: ist der Wert ein String → never (entfernt), sonst bleibt der Key-Name erhalten. id und age sind number und bleiben. name und email (string) werden entfernt.
Numbers?type Extract<T, U> = T extends U ? T : never;
type Numbers = Extract<string | number | boolean, number | boolean>;Extract<T, U> ist das Gegenstück zu Exclude — es behält aus der Union T nur die Typen, die U zuweisbar sind. string ist nicht in U, fällt also weg. Übrig bleibt number | boolean.
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
function add(a, b) {
return a + b;
}strict: true aktiviert unter anderem noImplicitAny. Die Parameter a und b haben keinen Typ und lassen sich aus dem Kontext nicht inferieren — der Compiler würde sie auf any setzen, das Flag verhindert das.
type ReadOnly<T> = { readonly [P in keyof T]: T[P] };
type MyPartial<T> = { [P in keyof T]?: T[P] };
interface Todo {
title: string;
done: boolean;
}
type A = ReadOnly<Todo>;
type B = MyPartial<Todo>;Mapped Types iterieren über die Keys eines Typs und erlauben das Hinzufügen von Modifiern. ReadOnly setzt readonly auf jedes Feld, MyPartial setzt ? (Optionalität). So funktionieren auch die eingebauten Readonly<T> und Partial<T>.
CSSSpacing und wie werden sie generiert?type CSSProperty = "margin" | "padding";
type Direction = "top" | "right" | "bottom" | "left";
type CSSSpacing = {
[K in `${CSSProperty}-${Direction}`]: string;
};
const spacing: CSSSpacing = {
"margin-top": "10px",
"margin-right": "20px",
// ... wymaga wszystkich 8 kombinacji
};Ein Template Literal Type in einem Mapped Type erzeugt ALLE Kombinationen: margin-top, margin-right, margin-bottom, margin-left, padding-top, padding-right, padding-bottom, padding-left = 8 Felder. TypeScript erzeugt das kartesische Produkt der Strings. Ein starkes Werkzeug für CSS-in-JS-Typen.
type Awaited<T> =
T extends Promise<infer U>
? Awaited<U>
: T;
type A = Awaited<Promise<string>>;
type B = Awaited<Promise<Promise<number>>>;
type C = Awaited<boolean>;Awaited 'unwrappt' Promises rekursiv: Promise<string> → string, Promise<Promise<number>> → Promise<number> → number. Ist T kein Promise, wird T unverändert zurückgegeben (boolean). Eine vereinfachte Variante des eingebauten Awaited<T> ab TypeScript 4.5.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
type B = ToArray<string | number | boolean>;Conditional Types mit Parameter T distribuieren über Unions! TypeScript wendet die Bedingung auf JEDEN Union-Member einzeln an: string → string[], number → number[]. Das Ergebnis ist eine Union von Arrays, kein Array von Unions. Um die Distribution zu unterbinden, packt man T in [T].
r und g, und wie unterscheidet sich satisfies von : Colors?type Colors = Record<string, [number, number, number] | string>;
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Colors;
const r = palette.red; // jaki typ?
const g = palette.green; // jaki typ?Der Operator satisfies (TS 4.9+) prüft, ob ein Wert zu einem Typ passt, weitet den Typ der Variable aber NICHT auf. Mit : Colors hätten beide Felder den Typ [number,number,number] | string. Mit satisfies behält TypeScript die engen Typen — red ist ein Tuple, green ein String.
StringFields und NumberFields?type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Model {
id: number;
name: string;
active: boolean;
email: string;
age: number;
}
type StringFields = PickByType<Model, string>;
type NumberFields = PickByType<Model, number>;Key Remapping mit as + Conditional Type erlaubt das Filtern von Keys. Ist T[K] extends U false, wird der Key auf never gemappt — und damit aus dem Ergebnis entfernt. StringFields enthält die Felder mit String-Werten, NumberFields die mit number-Werten.
console.log aus und wie heißt diese Technik?const add = (a: number) => (b: number) => a + b;
const add5 = add(5);
console.log(add5(3));Currying wandelt eine Funktion mit mehreren Argumenten in eine Folge einargumentiger Funktionen um. add(5) liefert (b) => 5 + b, also add5(3) = 8.
transform(5) zurück?const pipe = <T>(...fns: Array<(arg: T) => T>) =>
(value: T): T =>
fns.reduce((acc, fn) => fn(acc), value);
const transform = pipe(
(x: number) => x * 2,
(x: number) => x + 1,
(x: number) => x * 3
);
console.log(transform(5));Pipe führt die Funktionen von links nach rechts aus: 5 → *2 = 10 → +1 = 11 → *3 = 33.
type Option<T> = { tag: "some"; value: T } | { tag: "none" };
const map = <A, B>(opt: Option<A>, fn: (a: A) => B): Option<B> =>
opt.tag === "some"
? { tag: "some", value: fn(opt.value) }
: { tag: "none" };
const result = map({ tag: "some", value: 5 }, x => x * 2);Der Option- (Maybe-) Monad kapselt einen Wert, der möglicherweise nicht existiert. Die map-Funktion ist eine Functor-Operation — sie transformiert den Wert im Container, ohne die Struktur zu verändern.
const compose = <A, B, C>(
f: (b: B) => C,
g: (a: A) => B
) => (a: A): C => f(g(a));
const double = (x: number) => x * 2;
const toString = (x: number) => `Value: ${x}`;
const doubleThenString = compose(toString, double);
console.log(doubleThenString(5));Compose führt Funktionen von rechts nach links aus (mathematische Komposition f∘g). Pipe macht das Gegenteil — von links nach rechts.
flatMap-Operation im Kontext von Monaden?type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
const flatMap = <T, E, U>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> =>
result.ok ? fn(result.value) : result;
const parse = (s: string): Result<number, string> => {
const n = Number(s);
return isNaN(n)
? { ok: false, error: "Not a number" }
: { ok: true, value: n };
};flatMap (bind/chain) ist die fundamentale monadische Operation. Im Unterschied zu map liefert die Funktion einen neuen Container (Result), und flatMap 'flattet' das Ergebnis.
const 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;
};
const factorial = memoize((n: number): number =>
n <= 1 ? 1 : n * factorial(n - 1)
);Memoization merkt sich Ergebnisse einer Funktion für gegebene Argumente. Folgende Aufrufe mit denselben Argumenten geben das gecachte Ergebnis zurück, statt neu zu rechnen.
updated?type Lens<S, A> = {
get: (s: S) => A;
set: (a: A, s: S) => S;
};
const nameLens: Lens<{ name: string; age: number }, string> = {
get: (s) => s.name,
set: (a, s) => ({ ...s, name: a }),
};
const user = { name: "Alice", age: 30 };
const updated = nameLens.set("Bob", user);Lens ist ein FP-Pattern für unveränderliche Updates verschachtelter Strukturen. Set erstellt ein neues Objekt mit dem aktualisierten Feld und behält den Rest unverändert.
type Predicate<T> = (value: T) => boolean;
const and = <T>(...preds: Predicate<T>[]): Predicate<T> =>
(value) => preds.every(p => p(value));
const or = <T>(...preds: Predicate<T>[]): Predicate<T> =>
(value) => preds.some(p => p(value));
const not = <T>(pred: Predicate<T>): Predicate<T> =>
(value) => !pred(value);
const isEven: Predicate<number> = n => n % 2 === 0;
const isPositive: Predicate<number> = n => n > 0;
const isOddAndPositive = and(not(isEven), isPositive);
console.log(isOddAndPositive(3), isOddAndPositive(-3));Predicate Composition: 3 ist ungerade (not even) und positiv → true. -3 ist ungerade, aber nicht positiv → false.
Task?type Task<T> = {
fork: (reject: (e: Error) => void, resolve: (t: T) => void) => void;
};
const taskOf = <T>(value: T): Task<T> => ({
fork: (_, resolve) => resolve(value),
});
const taskMap = <A, B>(task: Task<A>, fn: (a: A) => B): Task<B> => ({
fork: (reject, resolve) =>
task.fork(reject, (a) => resolve(fn(a))),
});Task ist ein lazy Async Monad. Im Gegensatz zu Promise wird er nicht sofort ausgeführt — die Berechnung startet erst beim Aufruf von fork().
const trampoline = <T>(fn: () => T | (() => T)): T => {
let result = fn();
while (typeof result === "function") {
result = (result as () => T)();
}
return result;
};
const sumTo = (n: number, acc: number = 0): any =>
n === 0 ? acc : () => sumTo(n - 1, acc + n);
console.log(trampoline(() => sumTo(100000)));Trampoline wandelt Tail-Rekursion in eine while-Schleife um und vermeidet so Stack Overflow. Statt sich rekursiv aufzurufen, gibt sie einen Thunk (Funktion) zurück, der in der Schleife ausgeführt wird.
const double = (x: number): number => x * 2;
const result1 = double(5);
const result2 = double(5);
let counter = 0;
const impureDouble = (x: number): number => {
counter++;
return x * 2;
};Eine Pure Function erfüllt zwei Bedingungen: (1) gleicher Input → gleicher Output, (2) keine Side Effects. impureDouble mutiert die externe Variable counter, ist also impure. Referential Transparency bedeutet, dass man double(5) immer durch den Wert 10 ersetzen kann.
withLogging um und wie heißt diese Art Funktion?const withLogging = <T extends (...args: any[]) => any>(fn: T) =>
(...args: Parameters<T>): ReturnType<T> => {
console.log("Calling with:", args);
const result = fn(...args);
console.log("Result:", result);
return result;
};
const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);Eine Higher-Order Function (HOF) ist eine Funktion, die eine andere Funktion entgegennimmt oder zurückgibt. withLogging nimmt die Funktion fn und gibt eine neue mit hinzugefügtem Logging zurück — wie ein Decorator. In FP sind Funktionen First-Class Citizens.
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount());Ein Closure ist ein Mechanismus, bei dem eine Funktion Zugriff auf Variablen aus ihrem Definitions-Scope behält — selbst nachdem dieser Scope beendet ist. count lebt im Closure von createCounter.
result?type Either<L, R> =
| { tag: "left"; value: L }
| { tag: "right"; value: R };
const left = <L>(value: L): Either<L, never> =>
({ tag: "left", value });
const right = <R>(value: R): Either<never, R> =>
({ tag: "right", value });
const map = <L, A, B>(
either: Either<L, A>,
fn: (a: A) => B
): Either<L, B> =>
either.tag === "right"
? right(fn(either.value))
: either;
const result = map(right(10), x => x * 2);Either ist ein Monad mit zwei Varianten: Left (üblicherweise Fehler) und Right (Erfolg). Die map-Funktion transformiert den Wert nur in Right und reicht Left unverändert weiter — eine Alternative zu try/catch.
type Thunk<T> = () => T;
const lazyValue: Thunk<number> = () => {
console.log("Computing...");
return 42;
};
// Wartość nie jest jeszcze obliczona
console.log("Before");
const result = lazyValue();
console.log("After:", result);Ein Thunk kapselt einen Effekt/Computation in einer Funktion zur verzögerten Ausführung (Lazy Evaluation). Die Berechnung startet erst, wenn du den Thunk aufrufst — so behältst du die Kontrolle über den Zeitpunkt.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Podejście imperatywne
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
sum += numbers[i] * 2;
}
}
// Podejście deklaratywne
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0);Deklarativer Stil (FP) sagt 'was' zu tun ist: filtere die Geraden, verdopple, summiere. Imperativ sagt 'wie': iteriere, prüfe, addiere. Deklarativ ist lesbarer und weniger fehleranfällig.
safeDivide (partial) und totalDivide (total)?const safeDivide = (a: number, b: number): number | never => {
if (b === 0) throw new Error("Division by zero");
return a / b;
};
const totalDivide = (a: number, b: number): number =>
b === 0 ? 0 : a / b;Eine totale Funktion ist für jeden Input ihres Definitionsbereichs definiert — sie liefert immer ein Ergebnis. Eine partielle Funktion kann werfen (wie safeDivide für b=0). In FP bevorzugen wir totale Funktionen.
const idempotent = (arr: number[]): number[] =>
[...new Set(arr)].sort((a, b) => a - b);
console.log(idempotent([3, 1, 2, 1, 3]));
console.log(idempotent(idempotent([3, 1, 2, 1, 3])));Idempotenz: f(f(x)) === f(x). Der erste Aufruf entfernt Duplikate und sortiert das Array. Ein zweiter Aufruf auf dem bereits verarbeiteten Ergebnis liefert dasselbe Ergebnis. Beispiele: Math.abs, Array.sort auf bereits sortiertem Array.
result? Welche Array-Methoden sind in FP zentral und warum?const users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 },
];
const result = users
.filter(u => u.age >= 30)
.map(u => u.name)
.reduce((acc, name) => acc + ", " + name);filter (auswählen), map (transformieren) und reduce (zusammenfügen) sind grundlegende FP-Operationen auf Kollektionen. Sie sind deklarativ, mutieren die Quelle nicht und lassen sich zu einer Pipeline verketten. Ergebnis: Alice und Charlie sind >= 30.
result und fallback?const Maybe = <T>(value: T | null | undefined) => ({
map: <U>(fn: (val: T) => U) =>
value != null ? Maybe(fn(value)) : Maybe<U>(null),
getOrElse: (defaultVal: T) =>
value != null ? value : defaultVal,
});
const result = Maybe("hello")
.map(s => s.toUpperCase())
.map(s => s + "!")
.getOrElse("default");
const fallback = Maybe<string>(null)
.map(s => s.toUpperCase())
.getOrElse("default");Maybe behandelt null/undefined: existiert der Wert, transformiert map weiter. Ist er null — wird die ganze Kette übersprungen, und getOrElse liefert den Default-Wert. Eine Alternative zu langen if-null-Ketten.
result und warum bevorzugt FP diesen Stil?const users = [
{ name: "Alice", age: 30, role: "admin" },
{ name: "Bob", age: 25, role: "user" },
{ name: "Charlie", age: 35, role: "admin" },
{ name: "Diana", age: 28, role: "user" },
] as const;
const result = users
.filter(u => u.role === "admin")
.map(u => ({ ...u, name: u.name.toUpperCase() }))
.sort((a, b) => a.age - b.age);Die Pipeline filter→map→sort ist FP pur: jede Operation erzeugt eine neue Struktur, das Original bleibt unangetastet. Deklarativer Stil sagt, WAS du tust (Admins filtern, Namen großschreiben, nach Alter sortieren), nicht WIE. Leicht zu lesen, zu testen und zu debuggen.
type Fn<A, B> = (a: A) => B;
const pipe2 = <A, B, C>(
f: Fn<A, B>,
g: Fn<B, C>
): Fn<A, C> => (a) => g(f(a));
const parse = (s: string): number => parseInt(s, 10);
const double = (n: number): number => n * 2;
const format = (n: number): string => `Result: ${n}`;
const transform = pipe2(pipe2(parse, double), format);
console.log(transform("21"));pipe2 komponiert zwei Funktionen: f: A→B und g: B→C ergibt A→C. Generics garantieren passende Typen — du kannst string→number nicht mit boolean→string komponieren, weil B übereinstimmen muss. parse('21')=21, double(21)=42, format(42)='Result: 42'.
Tree<T>?type Tree<T> =
| { type: "leaf"; value: T }
| { type: "node"; left: Tree<T>; right: Tree<T> };
const sumTree = (tree: Tree<number>): number =>
tree.type === "leaf"
? tree.value
: sumTree(tree.left) + sumTree(tree.right);
const myTree: Tree<number> = {
type: "node",
left: { type: "leaf", value: 1 },
right: {
type: "node",
left: { type: "leaf", value: 2 },
right: { type: "leaf", value: 3 },
},
};
console.log(sumTree(myTree));Tree<T> ist ein Algebraic Data Type (ADT) — eine Discriminated Union mit rekursiver Definition. sumTree summiert rekursiv: node(leaf(1), node(leaf(2), leaf(3))) = 1 + (2 + 3) = 6. ADTs sind ein Fundament der FP — sie modellieren Datenstrukturen ohne Klassen, rein über Typen.
const map = <A, B>(arr: A[], fn: (a: A) => B): B[] =>
arr.map(fn);
const identity = <T>(x: T): T => x;
const double = (x: number) => x * 2;
const addOne = (x: number) => x + 1;
// Law 1: Identity
map([1, 2, 3], identity) // === [1, 2, 3]
// Law 2: Composition
map(map([1, 2, 3], double), addOne)
// ===
map([1, 2, 3], x => addOne(double(x)))Ein Functor ist ein Container (Array, Maybe, Promise) mit einer map-Operation, die zwei Gesetze erfüllt: (1) das Mappen der Identität ändert den Container nicht, (2) zwei Funktionen nacheinander zu mappen ergibt dasselbe wie ihre Komposition zu mappen. So bleibt das Verhalten vorhersehbar.
result und was ist ein Transducer?type Reducer<A, B> = (acc: B, val: A) => B;
type Transducer<A, B> = <C>(rf: Reducer<B, C>) => Reducer<A, C>;
const mapT = <A, B>(fn: (a: A) => B): Transducer<A, B> =>
(rf) => (acc, val) => rf(acc, fn(val));
const filterT = <A>(pred: (a: A) => boolean): Transducer<A, A> =>
(rf) => (acc, val) => pred(val) ? rf(acc, val) : acc;
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const isEven = (n: number) => n % 2 === 0;
const triple = (n: number) => n * 3;
const xform = (rf: Reducer<number, number[]>) =>
filterT(isEven)(mapT(triple)(rf));
const result = nums.reduce(
xform((acc, val) => [...acc, val]),
[] as number[]
);Ein Transducer kombiniert filter und map in einem Durchgang — ohne Zwischen-Arrays wie bei einer .filter().map()-Kette. Er filtert die Geraden (2,4,6,8,10) und multipliziert ×3 in einer einzigen Reduktion. Bei großen Datenmengen effizienter als Ketten.
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
}
const Button = ({ label, onClick, variant = "primary" }: ButtonProps) => (
<button onClick={onClick} className={variant}>
{label}
</button>
);Best Practice: Definiere ein Interface/Type für die Props und nutze es als Typ des Parameters. Optionale Props markierst du mit ?, Default-Werte gibst du beim Destrukturieren an.
setCount('hello') einen Fehler?const [count, setCount] = useState(0);
setCount("hello");TypeScript inferiert den State-Typ aus dem Startwert. useState(0) ergibt Typ number, daher akzeptiert setCount nur number. Für Union Types nutzt du useState<string | number>(0).
useState<User | null>(null) explizit angeben?const [user, setUser] = useState<User | null>(null);
if (user) {
console.log(user.name);
}null inferieren — es wüsste nichts von UserWenn der Startwert null ist, inferiert TypeScript den Typ als null. Der explizite Generic <User | null> sagt: der State kann User oder null sein. Nach if (user) engt TypeScript den Typ auf User ein.
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.value);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};React liefert eigene Event-Typen (React.MouseEvent, React.ChangeEvent, React.FormEvent usw.) mit einem Generic für das HTML-Element. Damit hast du vollen Zugriff auf die typisierten Properties des Elements über currentTarget.
useRef<HTMLInputElement>(null) einen Generic?const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;useRef<T>(null) erzeugt einen Ref mit Typ T | null. Der Generic sagt TypeScript, dass .current ein HTMLInputElement ist (oder null vor dem Mount). So bekommst du volles Autocomplete für die DOM-API.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
<List items={["a", "b"]} renderItem={(item) => <span>{item}</span>} />Eine Generic Component nutzt den Typparameter <T> sowohl im Props-Interface als auch in der Komponente. TypeScript inferiert T aus den übergebenen Daten — hier string[] → T = string. Das Rendering ist vollständig typisiert.
ThemeContextType | undefined typisiert?interface ThemeContextType {
theme: "light" | "dark";
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
}createContext braucht einen Default-Wert. Undefined + Custom Hook mit Guard ist ein sicheres Pattern: ist die Komponente nicht in einem Provider, wirft der Hook einen lesbaren Fehler, statt undefined zurückzugeben.
children und wie unterscheidet er sich von React.ReactElement?interface CardProps {
title: string;
children: React.ReactNode;
}
const Card = ({ title, children }: CardProps) => (
<div>
<h2>{title}</h2>
<div>{children}</div>
</div>
);React.ReactNode ist der breiteste Typ für children: JSX, String, Number, Boolean, null, undefined, Arrays. React.ReactElement ist strikt ein JSX-Element (<Component /> oder <div />). Für children fast immer ReactNode nehmen.
React.ComponentPropsWithoutRef<'button'>?type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
variant?: "primary" | "danger";
};
const Button = ({ variant = "primary", children, ...rest }: ButtonProps) => (
<button className={variant} {...rest}>
{children}
</button>
);ComponentPropsWithoutRef<'element'> extrahiert alle Props eines nativen HTML-Elements. In Kombination mit & kannst du eigene Props (variant) hinzufügen. Ideales Pattern für wiederverwendbare UI-Komponenten.
React.memo mit TypeScript?const MemoizedList = React.memo(function UserList({
users,
onSelect,
}: {
users: User[];
onSelect: (user: User) => void;
}) {
return (
<ul>
{users.map((u) => (
<li key={u.id} onClick={() => onSelect(u)}>{u.name}</li>
))}
</ul>
);
});React.memo() ist generisch und inferiert die Prop-Typen automatisch aus der gewrappten Komponente. TypeScript stellt sicher, dass der Prop-Vergleich (Shallow Comparison) typsicher ist.
dispatch von useReducer?type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "set"; payload: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "decrement": return state - 1;
case "set": return action.payload;
}
}
const [count, dispatch] = useReducer(reducer, 0);useReducer inferiert den Dispatch-Typ aus dem Action-Typ. Die Discriminated Union (Feld type) garantiert, dass dispatch nur gültige Actions annimmt. action.payload ist nur im Case 'set' verfügbar.
useFetch den Generic <T,>?const useFetch = <T,>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json: T) => setData(json))
.catch((err) => setError(err))
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
};
const { data } = useFetch<User[]>("/api/users");In .tsx-Dateien würde <T> als JSX-Tag interpretiert. <T,> ist der Workaround — das nachgestellte Komma unterscheidet den Generic von einem Tag. Der Caller schreibt useFetch<User[]>(url) und data hat den Typ User[] | null.
React.forwardRef?const ForwardedInput = React.forwardRef<
HTMLInputElement,
{ label: string }
>(({ label, ...props }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
));React.forwardRef<RefType, PropsType> — der erste Generic ist der DOM-Element-Typ (ref), der zweite der Props-Typ. So kann eine Child-Komponente eine Ref an ein inneres Element weiterreichen.
React.ReactNode und React.ReactElement in Props?type PropsWithChildren<P = {}> = P & {
children?: React.ReactNode;
};
interface LayoutProps {
sidebar: React.ReactElement;
}
const Layout = ({ sidebar, children }: PropsWithChildren<LayoutProps>) => (
<div>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);Nutze ReactNode für children (akzeptiert alles). Nutze ReactElement, wenn du eine konkrete JSX-Komponente verlangst (z. B. sidebar muss ein Element sein, kein Text). Wichtig beim Typisieren von Layouts.
AsyncState<T> besser als State<T> für die Verwaltung von Async-State?type Status = "idle" | "loading" | "success" | "error";
interface State<T> {
status: Status;
data: T | null;
error: string | null;
}
type AsyncState<T> =
| { status: "idle"; data: null; error: null }
| { status: "loading"; data: null; error: null }
| { status: "success"; data: T; error: null }
| { status: "error"; data: null; error: string };Eine Discriminated Union über status schließt unmögliche Zustände aus. TypeScript weiß: success → data: T (nicht null), error → error: string (nicht null). State<T> erlaubt error + data gleichzeitig — ein Bug, der nur darauf wartet zu passieren.
useCallback und warum braucht formData.get ein as string?const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string;
await submitForm({ email });
},
[]
);FormData.get() liefert string | File | null, weil Formulare auch Dateien enthalten können. Wenn du weißt, dass das Feld Text ist, engt as string den Typ ein. Alternative: ein Type Guard typeof val === 'string'.
| undefined im Gegensatz zum vorherigen Beispiel?type Theme = "light" | "dark";
const ThemeContext = createContext<Theme>("light");
const useTheme = () => useContext(ThemeContext);
const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<Theme>("light");
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
};Wenn createContext einen sinnvollen Default-Wert ("light") bekommt, erhält die Komponente auch ohne Provider diesen Wert. Kein undefined und kein Guard nötig. Das undefined-Pattern braucht man nur, wenn es KEINEN sinnvollen Default gibt.
type ButtonVariant = "primary" | "secondary" | "danger";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
isLoading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", isLoading, children, disabled, ...props }, ref) => (
<button
ref={ref}
disabled={disabled || isLoading}
className={variant}
{...props}
>
{isLoading ? "Loading..." : children}
</button>
)
);Das Pattern extends HTMLAttributes ist Standard in Design Systems. Die Komponente akzeptiert alle nativen <button>-Props (onClick, disabled, type usw.) + eigene (variant, isLoading). forwardRef ermöglicht das Weiterleiten der Ref.
const useLocalStorage = <T,>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
};Der Hook imitiert die useState-API — er liefert ein Tuple [value, setter]. Der Union Type T | ((prev: T) => T) erlaubt den Setter mit Wert oder Callback. Dasselbe Pattern wie das eingebaute setState.
keyof T im Typ Column<T>?type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
};
interface TableProps<T> {
data: T[];
columns: Column<T>[];
}
function Table<T extends Record<string, unknown>>({ data, columns }: TableProps<T>) {
return (
<table>
<thead>
<tr>{columns.map(col => <th key={String(col.key)}>{col.header}</th>)}</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map(col => (
<td key={String(col.key)}>
{col.render ? col.render(row[col.key], row) : String(row[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}key in einer Spalte ein echter Key des Typs T ist — du kannst kein nicht existierendes Feld angeben. TypeScript bietet Autocomplete für verfügbare Keyskeyof T in einer generischen Table-Komponente garantiert Typsicherheit: Spalten können nur existierende Datenfelder referenzieren. Ein neues Feld in den Daten ist sofort auch in den Spalten verfügbar.
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">;
function Box<E extends React.ElementType = "div">({
as,
children,
...props
}: PolymorphicProps<E>) {
const Component = as || "div";
return <Component {...props}>{children}</Component>;
}
<Box as="a" href="/about">Link</Box>
<Box as="button" onClick={() => {}}>Click</Box>
<Box>Default div</Box>as-Prop entgegen, um das HTML-Tag zu ändern — TypeScript typisiert die Props automatisch je nach gewähltem ElementEine Polymorphic Component (beliebt in Chakra UI, Radix) lässt das gerenderte Element über den as-Prop wechseln. Dank Generics weiß TypeScript: bei as='a' enthalten die Props href. Bei as='button' — onClick. Volle Typsicherheit.
{ data: T | null; loading: boolean; error: Error | null }?type QueryResult<T> =
| { status: "idle"; data: undefined; error: undefined }
| { status: "loading"; data: undefined; error: undefined }
| { status: "success"; data: T; error: undefined }
| { status: "error"; data: undefined; error: Error };
function useQuery<T>(url: string): QueryResult<T> {
// implementation
}
const result = useQuery<User[]>("/api/users");
if (result.status === "success") {
result.data.map(u => u.name); // TypeScript wie!
}Mit einem einfachen { data, loading, error } verhindert nichts unmögliche Zustände wie loading: true, data: someData, error: someError. Eine Discriminated Union schließt sie auf Typebene aus. Nach status === 'success' garantiert TypeScript, dass data T ist (nicht undefined).
interface DataListProps<T> {
items: T[];
filterFn?: (item: T) => boolean;
sortFn?: (a: T, b: T) => number;
children: (item: T, index: number) => React.ReactNode;
}
function DataList<T>({
items,
filterFn,
sortFn,
children,
}: DataListProps<T>) {
let processed = [...items];
if (filterFn) processed = processed.filter(filterFn);
if (sortFn) processed = processed.sort(sortFn);
return <>{processed.map((item, i) => children(item, i))}</>;
}
<DataList
items={users}
filterFn={u => u.active}
sortFn={(a, b) => a.name.localeCompare(b.name)}
>
{(user) => <UserCard key={user.id} user={user} />}
</DataList>DataList ist eine generische Komponente mit Render-Prop-Pattern. TypeScript inferiert T aus items und reicht es an filterFn, sortFn und children weiter. Trennt Logik (Filtern, Sortieren) von Darstellung (Render Prop). Reines FP in React.
handleChange('name', 42) und handleChange('unknown', 'value') einen Fehler?type FormFields = {
name: string;
email: string;
age: number;
};
const useForm = <T extends Record<string, unknown>>(initial: T) => {
const [values, setValues] = useState<T>(initial);
const handleChange = <K extends keyof T>(
field: K,
value: T[K]
) => {
setValues(prev => ({ ...prev, [field]: value }));
};
return { values, handleChange };
};
const { values, handleChange } = useForm<FormFields>({
name: "",
email: "",
age: 0,
});
handleChange("name", "Alice"); // OK
handleChange("age", 25); // OK
handleChange("name", 42); // Error!
handleChange("unknown", "value"); // Error!Der Generic K extends keyof T in handleChange erzeugt eine Abhängigkeit: field: K beschränkt auf existierende Keys, value: T[K] verlangt den passenden Typ. TypeScript prüft BEIDES gleichzeitig — weder ein falsches Feld noch ein falscher Werttyp ist erlaubt.
interface DialogProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
interface DialogComposition {
Header: React.FC<{ children: React.ReactNode }>;
Body: React.FC<{ children: React.ReactNode }>;
Footer: React.FC<{ children: React.ReactNode }>;
}
const Dialog: React.FC<DialogProps> & DialogComposition = ({
open,
onClose,
children,
}) => {
if (!open) return null;
return <div className="dialog">{children}</div>;
};
Dialog.Header = ({ children }) => <header>{children}</header>;
Dialog.Body = ({ children }) => <main>{children}</main>;
Dialog.Footer = ({ children }) => <footer>{children}</footer>;Das Compound-Component-Pattern (beliebt in Radix, Headless UI) nutzt Dot-Notation für Komposition. Der Typ FC<DialogProps> & DialogComposition ist eine Intersection — TypeScript weiß, dass Dialog zugleich Komponente ist und Sub-Komponenten hat. Sorgt für Autocomplete auf Dialog.