Skip to content

TypeScript Spickzettel

Typisierte Obermenge von JavaScript, die skaliert.

01

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.

typescript
// 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.ts

tsconfig.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.

typescript
{
  "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.

typescript
// 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 error

Strict-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.

typescript
// 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).

typescript
// 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/react
02

Grundtypen

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.

typescript
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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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 readonly

Literal- & 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.

typescript
// 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"
03

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.

typescript
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 | undefined

Index-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.

typescript
// 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.

typescript
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.

typescript
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.

typescript
// 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/aliases

Optional 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.

typescript
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();
04

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.

typescript
// 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.

typescript
// 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.

typescript
// 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 props

Nullable-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).

typescript
// 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.

typescript
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.

typescript
// 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];
};
05

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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
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.

typescript
// 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.

typescript
// 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];
}
06

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.

typescript
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: readonly

Access-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.

typescript
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.

typescript
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 abstract

Interfaces & 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.

typescript
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));    // -10

Getter & 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.

typescript
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; // throws

Statische 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.

typescript
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 constructor
07

Generics

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.

typescript
// 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>).

typescript
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.

typescript
// 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 key

Default-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.

typescript
// 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.

typescript
// 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.

typescript
// 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;
08

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.

typescript
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.

typescript
// 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.

typescript
// 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>; // string

Discriminated 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.

typescript
// 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.

typescript
// 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.

typescript
// '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
}
09

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.

typescript
// 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.

typescript
// 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.

typescript
// 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)[].

typescript
// 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); // Error

Enums & 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.

typescript
// 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.

typescript
// 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 object
10

Module & 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.

typescript
// 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.14159

Type-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.

typescript
// 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 avoid

Dynamische 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).

typescript
// 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.

typescript
// 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.

typescript
// 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 declarations

tsconfig-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.

typescript
{
  "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"
11

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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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;
}
12

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.

typescript
// 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.

typescript
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).

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
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'
13

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.

typescript
// 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).

typescript
// 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.

typescript
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.

typescript
// 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.

typescript
// 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 construction

Praktischer 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.

typescript
// 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");
14

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.

typescript
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 mutation

Pick, 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.

typescript
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.

typescript
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>; // Point

Exclude, 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.

typescript
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.

typescript
// 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];
15

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.

typescript
// 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 unions

infer-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.

typescript
// 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.

typescript
// 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.

typescript
// 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]); // number

Template-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.

typescript
// 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;
16

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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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.
17

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.

typescript
// 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.

typescript
// '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.

typescript
// 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.

typescript
// 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
// 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[])
  }
}
18

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
// 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.

typescript
// 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 needed

Bester 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.

typescript
// 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
// 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 toUpperCase

satisfies-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.

typescript
// '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}`;
19

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.

typescript
// 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 constants

Module-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.

typescript
// 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.

typescript
// 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.

typescript
// 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/search

Type-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.

typescript
// '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 }
// }
20

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.

typescript
{
  "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.

typescript
{
  "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.

typescript
{
  "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.ts

Project 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.

typescript
// 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.

typescript
// 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"]
}
21

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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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; }
        };
    };
}
22

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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
declare module 'vue' {
    interface ComponentCustomProperties {
        $auth: { login: () => Promise<void> };
    }
}
export default defineComponent({
    methods: { async login() { await this.$auth.login(); } }
});
23

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.

typescript
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.

typescript
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.

typescript
function Counter() { Counter.count++; }
namespace Counter {
    export let count = 0;
    export function reset() { count = 0; }
}
Counter(); Counter();
console.log(Counter.count);  // 2

Mit 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.

typescript
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.

typescript
// 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; }
24

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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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)
}
25

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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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!
26

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.

typescript
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.

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>>>;  // boolean

Array-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.

typescript
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]>;  // string

Funktionsparameter 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.

typescript
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.

typescript
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 }
27

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.

typescript
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 runtime

Kontravarianz

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.

typescript
type Handler<T> = (arg: T) => void;
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);
let animalHandler: Handler<Animal> = dogHandler;  // OK with strictFunctionTypes

Bivarianz

Methoden-Syntax ist bivariant. Function-Property-Syntax ist kontravariant mit strictFunctionTypes. Methoden sind bivariant für OO-Kompatibilität.

typescript
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).

typescript
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.

typescript
interface Box<T> { get(): T; set(value: T): void; }
let dogBox: Box<Dog> = {} as any;
let animalBox: Box<Animal> = dogBox;  // Error: invariant
28

Builder-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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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 }; }
}
29

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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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;
        }));
    });
});
30

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).

typescript
// 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.

typescript
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;  // OK

Enum vs Union

Enums erstellen Runtime-Objekte mit Reverse-Mapping. Union-Typen sind Zero-Runtime und tree-shakeable. Bevorzuge Union-Typen für neuen Code.

typescript
// 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.

typescript
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.

typescript
// 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; }

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.