TypeScript für JavaScript-Entwickler – Migration & Best Practices (2025)
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.

~20 Min. Lesezeit · Veröffentlicht am
npm install --save-dev typescript @types/node
npx tsc --init
# tsconfig.json anpassen (siehe unten)
mv index.js index.ts
npx tscDas 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:
- 15% weniger Bugs in Production (Airbnb Case Study)
- Refactoring 2-3x schneller durch IDE-Support
- Onboarding neuer Entwickler 40% schneller
- Selbstdokumentierender Code reduziert Dokumentationsaufwand
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() };
} - 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"]
} Bei der Migration kann strict: true überwältigend sein. Starte mit:
"strict": false,
"strictNullChecks": true, // Schrittweise aktivieren
"strictFunctionTypes": true,
"strictBindCallApply": trueMigration-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 - ☐ 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: 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 // 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
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 }; - Woche 1-2: Basics (Types, Interfaces, Functions)
- Woche 3-4: Generics & Utility Types
- Woche 5-6: Advanced Patterns (Conditional Types, Mapped Types)
- Woche 7-8: Real-world Integration (Build Tools, Libraries)
- 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
1. skipLibCheck aktivieren
// node_modules nicht prüfen = 30-50% schneller
"skipLibCheck": true2. 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:
- Schrittweise migrieren: Nicht alles auf einmal, sondern Phase für Phase
- Strict Mode als Ziel: Aber nicht von Anfang an erzwingen
- Types first: Erst Types definieren, dann implementieren
- Utility Types nutzen: Rad nicht neu erfinden
- Team-Konventionen: Einheitlicher Style wichtiger als "perfekt"
- Official TypeScript Docs
- Type Challenges – TypeScript Puzzles
- Total TypeScript – Matt Pocock's Course
- TypeScript Deep Dive – Kostenloses E-Book
- TypeScript Performance Wiki
# 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