Zum Inhalt springen

TypeScript für JavaScript-Entwickler – Migration & Best Practices (2025)

· von

TypeScript bringt Type-Safety in deine JavaScript-Projekte. In diesem Guide zeige ich dir, wie du bestehende Projekte schrittweise migrierst, das Type System effektiv nutzt und typische Anfängerfehler vermeidest.

TypeScript Migration Guide – Übersicht

~20 Min. Lesezeit · Veröffentlicht am

🚀 Quick Start für Ungeduldige
npm install --save-dev typescript @types/node
npx tsc --init
# tsconfig.json anpassen (siehe unten)
mv index.js index.ts
npx tsc

Das war's! Du hast dein erstes TypeScript-Projekt. Jetzt lass uns ins Detail gehen...

Warum TypeScript? Der Business Case

Bevor wir technisch werden, lass uns über den ROI von TypeScript sprechen. Studien zeigen:

TypeScript vs JSDoc – Was ist besser?

// JSDoc Approach
/**
 * @param {string} name
 * @param {number} age
 * @returns {User}
 */
function createUser(name, age) {
  return { name, age, id: Math.random() };
}

// TypeScript Approach
interface User {
  id: number;
  name: string;
  age: number;
}

function createUser(name: string, age: number): User {
  return { name, age, id: Math.random() };
}
💡 Wann TypeScript, wann JSDoc?
  • TypeScript: Neue Projekte, Teams > 3 Personen, komplexe Business-Logik
  • JSDoc: Kleine Libraries, wenn Build-Step vermieden werden soll
  • Hybrid: TypeScript für App-Code, JSDoc für Config-Files

Setup & Konfiguration

Installation und Initialisierung

# TypeScript und nötige Types installieren
npm install --save-dev typescript @types/node

# tsconfig.json generieren
npx tsc --init

# Für React-Projekte
npm install --save-dev @types/react @types/react-dom

# Für Express
npm install --save-dev @types/express @types/cors

Die perfekte tsconfig.json

{
  "compilerOptions": {
    // Target & Module
    "target": "ES2022",              // Modernes JavaScript
    "module": "ESNext",               // ESM Modules
    "lib": ["ES2022", "DOM"],         // Verfügbare APIs
    
    // Module Resolution
    "moduleResolution": "bundler",    // Für Vite/Webpack/ESBuild
    "resolveJsonModule": true,        // JSON imports erlauben
    "allowImportingTsExtensions": true, // .ts imports (mit bundler)
    
    // Emit
    "outDir": "./dist",               // Output-Verzeichnis
    "rootDir": "./src",               // Source-Verzeichnis
    "declaration": true,              // .d.ts Dateien generieren
    "declarationMap": true,           // Source maps für .d.ts
    "sourceMap": true,                // Source maps für debugging
    
    // Type Checking
    "strict": true,                   // Alle strict flags
    "noUnusedLocals": true,           // Ungenutzte Variablen
    "noUnusedParameters": true,       // Ungenutzte Parameter
    "noImplicitReturns": true,        // Explizite returns
    "noFallthroughCasesInSwitch": true, // Switch cases
    
    // Interop
    "esModuleInterop": true,          // CommonJS interop
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    
    // Skip Lib Check (Performance)
    "skipLibCheck": true              // node_modules nicht prüfen
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
⚠️ Vorsicht bei strict: true

Bei der Migration kann strict: true überwältigend sein. Starte mit:

"strict": false,
"strictNullChecks": true,  // Schrittweise aktivieren
"strictFunctionTypes": true,
"strictBindCallApply": true

Migration-Strategie

4-Phasen Migration Plan

Phase 1: Preparation (Tag 1-2)

// 1. TypeScript installieren
npm install --save-dev typescript

// 2. Loose tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,          // JS und TS mischen
    "checkJs": false,         // JS nicht prüfen
    "strict": false,          // Noch nicht strict
    "noEmit": true           // Nur Type-Checking
  }
}

// 3. Rename entry point
mv src/index.js src/index.ts

// 4. Build-Script anpassen
"scripts": {
  "type-check": "tsc --noEmit",
  "build": "node build.js && tsc"
}

Phase 2: Core Migration (Woche 1)

// Kritische Module zuerst
// 1. Shared Types definieren
// src/types/index.ts
export interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'user';
}

export interface ApiResponse {
  data: T;
  error?: string;
  status: number;
}

// 2. Utils und Helpers migrieren
// src/utils/validation.js → validation.ts
export function validateEmail(email: string): boolean {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

// 3. API Layer
// src/api/users.js → users.ts
import type { User, ApiResponse } from '../types';

export async function fetchUser(id: string): Promise> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

Phase 3: Gradual Strictness (Woche 2-3)

// tsconfig.json schrittweise verschärfen
{
  "compilerOptions": {
    // Woche 2
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    
    // Woche 3
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    
    // Woche 4
    "strict": true, // Alle strict flags
    "noImplicitAny": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

Phase 4: Optimization (Woche 4+)

// Advanced TypeScript Features nutzen
// 1. Branded Types für Domain Modeling
type UserId = string & { __brand: 'UserId' };
type ProductId = string & { __brand: 'ProductId' };

// Verhindert Verwechslungen
function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }

// 2. Template Literal Types
type EventName = `on${Capitalize}`;
type ClickHandler = `onClick${string}`;

// 3. Const Assertions
const config = {
  api: 'https://api.example.com',
  timeout: 5000
} as const; // Alles wird readonly
📊 Migration Checklist
  • ☐ TypeScript & Types installiert
  • ☐ tsconfig.json konfiguriert
  • ☐ Entry point zu .ts renamed
  • ☐ Build-Pipeline angepasst
  • ☐ Shared Types definiert
  • ☐ Core modules migriert
  • ☐ Strict mode aktiviert
  • ☐ CI/CD updated

Type System Basics

Primitive Types & Basics

// Primitive Types
let name: string = "Sven";
let age: number = 30;
let isActive: boolean = true;
let data: null = null;
let id: undefined = undefined;
let uniqueId: symbol = Symbol('id');
let bigNumber: bigint = 100n;

// Arrays
let numbers: number[] = [1, 2, 3];
let strings: Array = ["a", "b", "c"];

// Tuples
let user: [string, number] = ["Sven", 30];
let rgb: [number, number, number] = [255, 0, 0];

// Objects
let person: { name: string; age: number } = {
  name: "Sven",
  age: 30
};

// Functions
let greet: (name: string) => void;
greet = (name) => console.log(`Hello ${name}`);

// Any vs Unknown
let anything: any = 42;        // ⚠️ Vermeiden!
let something: unknown = 42;   // ✅ Sicherer

// Type Assertions
let value: unknown = "Hello World";
let length = (value as string).length;

Interfaces vs Types

// Interface - für Objekt-Shapes
interface User {
  id: number;
  name: string;
  email: string;
  age?: number;              // Optional
  readonly createdAt: Date;  // Readonly
}

// Type - für Unions, Intersections, Primitives
type Status = 'pending' | 'active' | 'inactive';
type ID = string | number;

// Interface Extension
interface Admin extends User {
  role: 'admin';
  permissions: string[];
}

// Type Intersection
type AdminUser = User & {
  role: 'admin';
  permissions: string[];
};

// Interface Merging (Declaration Merging)
interface Window {
  myCustomProperty: string;
}
// Wird mit globalem Window gemerged

// Type Alias für Functions
type ClickHandler = (event: MouseEvent) => void;
type AsyncFunction = () => Promise;
📝 Interface vs Type - Wann was?
  • Interface: Objekt-Shapes, APIs, Classes, Declaration Merging needed
  • Type: Unions, Intersections, Tuple Types, Function Types, Mapped Types
  • Tipp: Im Team einheitlich bleiben ist wichtiger als die "perfekte" Wahl

Advanced Types & Patterns

Union & Intersection Types

// Union Types
type Result = 
  | { success: true; data: T }
  | { success: false; error: string };

function handleResult(result: Result) {
  if (result.success) {
    console.log(result.data);  // TypeScript weiß: data existiert
  } else {
    console.log(result.error); // TypeScript weiß: error existiert
  }
}

// Discriminated Unions
type Action = 
  | { type: 'INCREMENT'; payload: number }
  | { type: 'DECREMENT'; payload: number }
  | { type: 'RESET' };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'INCREMENT':
      return state + action.payload; // payload verfügbar
    case 'DECREMENT':
      return state - action.payload;
    case 'RESET':
      return 0; // Kein payload hier
  }
}

Generics richtig nutzen

// Basic Generic Function
function identity(value: T): T {
  return value;
}

// Generic mit Constraints
interface HasLength {
  length: number;
}

function logLength(value: T): T {
  console.log(value.length);
  return value;
}

// Generic Classes
class DataStore {
  private data: T[] = [];
  
  add(item: T): void {
    this.data.push(item);
  }
  
  get(index: number): T | undefined {
    return this.data[index];
  }
  
  getAll(): T[] {
    return [...this.data];
  }
}

// Utility Generic Pattern
type Nullable = T | null;
type Optional = T | undefined;
type AsyncData = {
  loading: boolean;
  error?: string;
  data?: T;
};

Type Guards & Narrowing

// typeof Guard
function processValue(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase(); // string methods verfügbar
  }
  return value.toFixed(2); // number methods verfügbar
}

// instanceof Guard
class Dog {
  bark() { return "Woof!"; }
}
class Cat {
  meow() { return "Meow!"; }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    return animal.bark();
  }
  return animal.meow();
}

// Custom Type Guards
interface User {
  type: 'user';
  name: string;
}
interface Admin {
  type: 'admin';
  name: string;
  permissions: string[];
}

function isAdmin(user: User | Admin): user is Admin {
  return user.type === 'admin';
}

// in operator narrowing
type Car = { drive: () => void };
type Boat = { sail: () => void };

function operate(vehicle: Car | Boat) {
  if ('drive' in vehicle) {
    vehicle.drive();
  } else {
    vehicle.sail();
  }
}

Utility Types meistern

TypeScript bietet viele eingebaute Utility Types, die deinen Code drastisch vereinfachen:

// Partial - Alle Properties optional
interface User {
  id: string;
  name: string;
  email: string;
}

type PartialUser = Partial;
// { id?: string; name?: string; email?: string; }

function updateUser(id: string, updates: Partial) {
  // Updates können nur Teile enthalten
}

// Required - Alle Properties required
type RequiredUser = Required;

// Readonly - Alle Properties readonly
type ReadonlyUser = Readonly;

// Pick - Nur bestimmte Properties
type UserCredentials = Pick;
// { email: string; id: string; }

// Omit - Properties ausschließen
type PublicUser = Omit;
// { id: string; name: string; }

// Record - Object mit Keys und Values
type Roles = 'admin' | 'user' | 'guest';
type RolePermissions = Record;
// { admin: string[]; user: string[]; guest: string[]; }

// ReturnType - Return Type einer Function
function getUser() {
  return { id: '1', name: 'Sven' };
}
type UserType = ReturnType;

// Parameters - Parameter Types einer Function
function createUser(name: string, age: number) {}
type CreateUserParams = Parameters;
// [string, number]

// Awaited - Type aus Promise extrahieren
type PromiseUser = Promise;
type ResolvedUser = Awaited; // User
🎯 Custom Utility Types
// DeepPartial - Rekursiv partial
type DeepPartial = {
  [P in keyof T]?: T[P] extends object ? DeepPartial : T[P];
};

// DeepReadonly
type DeepReadonly = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P];
};

// Mutable - Readonly entfernen
type Mutable = {
  -readonly [P in keyof T]: T[P];
};

Third-Party Libraries typen

Types installieren

# Die meisten populären Libraries haben @types packages
npm install --save-dev @types/lodash
npm install --save-dev @types/express
npm install --save-dev @types/jest

# Check ob Types existieren
npm info @types/[package-name]

Fehlende Types selbst schreiben

// src/types/untyped-module.d.ts
declare module 'untyped-module' {
  export function doSomething(value: string): number;
  export class SomeClass {
    constructor(options: { timeout?: number });
    method(): Promise;
  }
}

// Für Module ohne Exports
declare module 'side-effect-module';

// Global augmentation
declare global {
  interface Window {
    gtag: (...args: any[]) => void;
    myApp: {
      version: string;
      config: Record;
    };
  }
}

// Ambient declarations für Assets
declare module '*.svg' {
  const content: string;
  export default content;
}

declare module '*.css' {
  const classes: { [key: string]: string };
  export default classes;
}

Build-Tools Integration

Vite + TypeScript

// vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
      '@types': resolve(__dirname, './src/types'),
    }
  },
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyLib',
      fileName: 'my-lib'
    }
  }
});

// tsconfig.json für Vite
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx", // Für React
    "strict": true,
    "paths": {
      "@/*": ["./src/*"],
      "@types/*": ["./src/types/*"]
    }
  }
}

Node.js + tsx

# tsx für Development (kein Build nötig)
npm install --save-dev tsx

# package.json scripts
{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "tsx src/index.ts",
    "build": "tsc",
    "type-check": "tsc --noEmit"
  }
}

// tsconfig.json für Node.js
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "ts-node": {
    "esm": true
  }
}

ESBuild für Production

// build.mjs
import esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  minify: true,
  sourcemap: true,
  target: 'es2020',
  outfile: 'dist/bundle.js',
  format: 'esm',
  // TypeScript specific
  tsconfig: 'tsconfig.json',
  treeShaking: true,
});

Häufige Fehler vermeiden

❌ Die Top 10 TypeScript Anfängerfehler

1. any überall verwenden

// ❌ Schlecht
let data: any = fetchData();

// ✅ Besser
let data: unknown = fetchData();
if (typeof data === 'object' && data !== null) {
  // Type narrowing
}

2. Type Assertions missbrauchen

// ❌ Gefährlich
const user = {} as User; // Lügt TypeScript an

// ✅ Sicher
const user: Partial = {};
// Oder validieren
if (isValidUser(data)) {
  const user: User = data;
}

3. Enum statt Union Types

// ❌ Unnötig komplex
enum Status {
  Pending = 'PENDING',
  Active = 'ACTIVE'
}

// ✅ Simpler
type Status = 'PENDING' | 'ACTIVE';

4. Interface Pollution

// ❌ Zu viele Interfaces
interface UserName { name: string; }
interface UserEmail { email: string; }
interface UserId { id: string; }

// ✅ Sinnvoll gruppieren
interface User {
  id: string;
  name: string;
  email: string;
}

Best Practices & Tips

1. Strict Mode von Anfang an

// Bei neuen Projekten immer
{
  "compilerOptions": {
    "strict": true
  }
}

2. Type-First Development

// Erst Types definieren
interface CreateUserInput {
  name: string;
  email: string;
  password: string;
}

interface CreateUserOutput {
  user: User;
  token: string;
}

// Dann implementieren
async function createUser(
  input: CreateUserInput
): Promise {
  // Implementation mit Type-Safety
}

3. Branded Types für Domain Modeling

// Verhindert Typ-Verwechslungen
type Email = string & { __brand: 'Email' };
type UserId = string & { __brand: 'UserId' };

function sendEmail(to: Email, subject: string) {}
function getUser(id: UserId) {}

// Helper für Branding
function asEmail(email: string): Email {
  if (!email.includes('@')) throw new Error('Invalid email');
  return email as Email;
}

4. Template Literal Types nutzen

// Typsichere String-Patterns
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiEndpoint = `/api/${string}`;

type Color = `#${string}`;
type Percentage = `${number}%`;

// Event Handler Types
type EventHandler = `on${Capitalize}`;

5. Conditional Types für Flexibilität

// Type abhängig von anderem Type
type IsArray = T extends any[] ? true : false;

type Flatten = T extends Array ? U : T;

// Praktisches Beispiel
type ApiResponse = T extends null 
  ? { success: false; error: string }
  : { success: true; data: T };
📚 TypeScript Learning Path
  1. Woche 1-2: Basics (Types, Interfaces, Functions)
  2. Woche 3-4: Generics & Utility Types
  3. Woche 5-6: Advanced Patterns (Conditional Types, Mapped Types)
  4. Woche 7-8: Real-world Integration (Build Tools, Libraries)
  5. Ongoing: Best Practices & Performance Optimization

Praktisches Migrations-Beispiel

Lass uns eine echte Express-App von JavaScript zu TypeScript migrieren:

Vorher (JavaScript)

// src/server.js
const express = require('express');
const cors = require('cors');

const app = express();

app.use(cors());
app.use(express.json());

// Routes
app.get('/api/users', async (req, res) => {
  const users = await getUsers();
  res.json(users);
});

app.post('/api/users', async (req, res) => {
  const user = await createUser(req.body);
  res.status(201).json(user);
});

app.listen(3000);

Nachher (TypeScript)

// src/types/index.ts
export interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

export interface CreateUserDto {
  name: string;
  email: string;
  password: string;
}

// src/server.ts
import express, { Request, Response } from 'express';
import cors from 'cors';
import { User, CreateUserDto } from './types';

const app = express();

app.use(cors());
app.use(express.json());

// Typed Route Handlers
app.get('/api/users', async (req: Request, res: Response) => {
  const users = await getUsers();
  res.json(users);
});

app.post('/api/users', async (
  req: Request<{}, {}, CreateUserDto>,
  res: Response
) => {
  const { name, email, password } = req.body;
  
  // Validation mit Type Guards
  if (!name || !email || !password) {
    return res.status(400).json({ 
      error: 'Missing required fields' 
    } as any);
  }
  
  const user = await createUser(req.body);
  res.status(201).json(user);
});

// Typed Error Handler
app.use((err: Error, req: Request, res: Response, next: any) => {
  console.error(err.stack);
  res.status(500).json({ error: err.message });
});

const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Performance-Tipps

⚡ TypeScript Performance optimieren

1. skipLibCheck aktivieren

// node_modules nicht prüfen = 30-50% schneller
"skipLibCheck": true

2. Incremental Compilation

// Build-Cache nutzen
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo"

3. Project References für große Projekte

// tsconfig.json
"references": [
  { "path": "./packages/shared" },
  { "path": "./packages/api" },
  { "path": "./packages/web" }
]

4. Exclude richtig setzen

"exclude": [
  "node_modules",
  "dist",
  "coverage",
  "**/*.test.ts",
  "**/*.spec.ts"
]

Debugging TypeScript

// VS Code launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug TypeScript",
      "runtimeArgs": ["-r", "tsx/cjs"],
      "program": "${workspaceFolder}/src/index.ts",
      "skipFiles": ["/**"],
      "sourceMaps": true,
      "cwd": "${workspaceFolder}",
      "env": {
        "NODE_ENV": "development"
      }
    }
  ]
}

Fazit & Next Steps

TypeScript ist eine Investition, die sich auszahlt. Die wichtigsten Takeaways:

📚 Weiterführende Resources
🎯 TypeScript Cheatsheet
# Quick Reference

## Types
string, number, boolean, null, undefined, symbol, bigint
any, unknown, never, void

## Arrays & Tuples
number[], Array
[string, number]

## Functions
(x: number) => string
(...args: string[]) => void

## Objects
{ x: number; y?: string; readonly z: boolean }

## Unions & Intersections
string | number
Type1 & Type2

## Type Guards
typeof x === 'string'
x instanceof MyClass
'property' in object
custom is Type

## Utility Types
Partial, Required, Readonly
Pick, Omit, Record
ReturnType, Parameters, Awaited

## Generics function fn(x: T): T class Store {} interface Container {} ## Advanced keyof T, T[K] T extends U ? X : Y infer R

Geschrieben von Sven Muscheid ·

Fragen zu TypeScript? Schreib mir!