Next.js TypeScript-Konfiguration im Detail: tsconfig optimieren und Typsicherheit in der Praxis

In den Testberichten leuchtet eine rote Zeile: „Production Error: Cannot read property ‘id’ of undefined”. Nutzer melden, dass die Profilseite nach einem Klick komplett weiß bleibt. Beim Durchsehen des Codes fällt auf: Die Route lautet /users/profile statt /user/profile – ein zusätzliches „s”. TypeScript meldet nichts, die IDE zeigt keinen Fehler, und so geht der Bug live.
Ein trivialer Fehler? Ja. Aber genau solche „Trivialfehler” tauchen in Projekten, die ich betreue, erstaunlich oft auf. Tippfehler in Routen, falsche Umgebungsvariablennamen, Funktionsparameter voller any – TypeScript verspricht Typsicherheit, fühlt sich in der Praxis aber oft wie JavaScript an.
Später wurde klar: TypeScript ist nicht das Problem – die Konfiguration war es. In tsconfig.json gibt es Dutzende Optionen, und man weiß nicht, welche man aktivieren oder deaktivieren soll. Tutorials widersprechen sich: Manche warnen vor strict mode, andere sagen, ohne strict mode lohnt sich TypeScript nicht. Nach fast einem Jahr Next.js + TypeScript wimmelte das Projekt noch von any-Typen.
In diesem Artikel teile ich die Erfahrungen aus einem Jahr voller Stolpersteine: von der optimierten tsconfig über typsichere Routen bis zu typisierten Umgebungsvariablen – Schritt für Schritt wird TypeScript vom Hindernis zum Schutzengel. Keine abstrakte Theorie, sondern Dinge, die Sie sofort nutzen können.
tsconfig optimieren – das Fundament legen
Was strict mode wirklich bedeutet
Viele (früher auch ich) halten strict: true für einen einfachen Schalter: Anmachen, und TypeScript wird streng. Stimmt nicht.
In der offiziellen TypeScript-Dokumentation steht: strict ist eine Kurzform für 7 Compiler-Optionen:
{
"compilerOptions": {
"strict": true,
// Entspricht allen 7 Optionen auf true
"strictNullChecks": true, // Strikte Null-Prüfung
"strictFunctionTypes": true, // Strikte Funktionstypen
"strictBindCallApply": true, // Strikte bind/call/apply-Prüfung
"strictPropertyInitialization": true, // Strikte Property-Initialisierung
"noImplicitAny": true, // Implizites any verbieten
"noImplicitThis": true, // Implizites this verbieten
"alwaysStrict": true // Immer im Strict Mode parsen
}
}
Besonders wichtig sind die ersten drei. strictNullChecks behandelt null und undefined als eigene Typen – nicht als legitime Werte jedes Typs.
Beispiel: Sie laden Benutzerdaten aus der Datenbank:
// Ohne strictNullChecks
const user = await db.user.findOne({ id: userId })
console.log(user.name) // TypeScript meldet nichts, user kann aber null sein
// Mit strictNullChecks
const user = await db.user.findOne({ id: userId })
console.log(user.name) // ❌ TypeScript-Fehler: Objekt kann null sein
// So muss es aussehen
if (user) {
console.log(user.name) // ✅ OK
}
Beim ersten Aktivieren in einem alten Projekt explodierten im IDE plötzlich über 200 rote Wellenlinien. Fast hätte ich alles wieder ausgeschaltet. Beim genaueren Hinsehen waren das aber echte potenzielle Bugs – Stellen ohne Null-Check, die in Produktion wirklich Probleme machen.
noImplicitAny ist ebenfalls zentral: Es verbietet, dass Parameter oder Variablen implizit any werden:
// Ohne noImplicitAny
function handleData(data) { // data wird automatisch any
return data.value // Kein Fehler bei beliebigen Operationen
}
// Mit noImplicitAny
function handleData(data) { // ❌ Fehler: Parameter hat implizit any
return data.value
}
// Explizite Annotation nötig
function handleData(data: { value: string }) { // ✅
return data.value
}
Am Anfang wirkt das umständlich. Früher schrieb man Funktionen schnell hin, jetzt braucht man Typen. Nach einiger Zeit merken Sie: Die IDE wird deutlich hilfreicher – bei data. erscheinen sofort alle Properties, ohne in der Doku nachzuschlagen.
TypeScript-Konfiguration speziell für Next.js
Die tsconfig.json in Next.js-Projekten hat einige Besonderheiten. Hier eine bewährte Konfiguration:
{
"compilerOptions": {
// Basis
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "bundler",
// Next.js erforderlich
"allowJs": true,
"noEmit": true,
"esModuleInterop": true,
"isolatedModules": true,
"resolveJsonModule": true,
// Strict mode (Kern)
"strict": true,
"skipLibCheck": true,
// Performance
"incremental": true,
// Next.js-Plugin
"plugins": [
{
"name": "next"
}
],
// Pfad-Aliase
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
"@/styles/*": ["./src/styles/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}
Einige oft übersehene Punkte:
1. incremental: Inkrementelle Kompilierung
Diese Option beschleunigt große Projekte deutlich. TypeScript cached den letzten Build und kompiliert beim nächsten Mal nur geänderte Dateien. In einem Projekt mit über 300 Komponenten sank die Kompilierzeit von etwa 45 auf 18 Sekunden – spürbar.
2. paths: Pfad-Aliase
Früher sah ein Import so aus:
import Button from '../../../components/ui/Button'
import { formatDate } from '../../../../lib/utils'
Unzählige .., und jede Ordnerverschiebung bricht alles.
Mit Aliases:
import Button from '@/components/ui/Button'
import { formatDate } from '@/lib/utils'
Viel klarer. TypeScript leitet Typen korrekt ab, und „Go to Definition” funktioniert.
3. plugins: Next.js-Plugin
"plugins": [{ "name": "next" }] wirkt simpel, lässt TypeScript aber Next.js-spezifische Dinge verstehen – z. B. Typen für layout.tsx und page.tsx unter app, sowie die Unterscheidung zwischen Server- und Client-Komponenten.
Ohne dieses Plugin meldet TypeScript bei Server-Komponenten oft falsche Typfehler.
Strict mode schrittweise aktivieren
Läuft das Projekt schon eine Weile mit viel Code, ist strict: true auf einmal oft schmerzhaft. Mein Rat: nicht mit Gewalt.
Strategie 1: Neuer Code strikt, alter Code nach und nach
In tsconfig.json strict: true lassen. Für alte Dateien, die noch nicht angepasst sind, oben in der Datei:
// @ts-nocheck // Gesamte Datei von der Typprüfung ausnehmen
Oder gezielt für eine Zeile:
// @ts-ignore // Nächste Zeile ignorieren
@ts-ignore und @ts-expect-error unterscheiden sich:
// @ts-ignore
const x = 1 as any // Kein Fehler, auch wenn die nächste Zeile keinen hat
// @ts-expect-error
const y = 1 // Warnt, wenn die nächste Zeile keinen Fehler mehr hat
Ich bevorzuge @ts-expect-error: Nach dem Fix erinnert TypeScript daran, den Kommentar zu entfernen.
Strategie 2: Strict pro Modul
Zuerst components aufräumen, andere Verzeichnisse vorerst locker lassen:
// tsconfig.strict.json (Strict mode)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"strict": true
},
"include": ["src/components/**/*"]
}
Im Alltag die normale tsconfig.json nutzen; beim Refactoring eines Moduls auf die Strict-Variante wechseln.
Strict mode ist nicht da, um zu nerven. Beim Refactoring einer alten Komponente mit strictNullChecks fand ich fünf fehlende Null-Checks – drei davon hatten in Produktion schon Fehler ausgelöst, die try-catch verschluckt hatte. Plötzlich wirkten die roten Wellenlinien fast freundlich.
Typsichere Routen – Schluss mit Tippfehlern
Next.js Typed Routes
Der Bug am Anfang: ein extra „s” in der Route, Seite 404. Solche Fehler lassen sich vermeiden.
Seit Next.js 13 gibt es die experimentelle Funktion typedRoutes. Aktiviert generiert TypeScript Typdefinitionen für alle Routen.
Aktivierung
In next.config.ts eine Zeile ergänzen:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
typedRoutes: true, // Typsichere Routen aktivieren
},
}
export default nextConfig
Dev-Server neu starten (npm run dev). Next.js scannt app und schreibt Routentypen nach .next/types.
Wie sieht das aus?
Projektstruktur:
app/
├── page.tsx // Startseite
├── blog/
│ ├── page.tsx // Blog-Liste
│ └── [slug]/
│ └── page.tsx // Blog-Detail
└── user/
└── [id]/
└── profile/
└── page.tsx // Benutzerprofil
Mit typedRoutes bietet die IDE in Link und useRouter Autovervollständigung:
import Link from 'next/link'
export default function Nav() {
return (
<nav>
<Link href="/">Startseite</Link>
<Link href="/blog">Blog</Link>
<Link href="/blog/hello-world">Artikel</Link>
<Link href="/user/123/profile">Profil</Link>
{/* ❌ TypeScript-Fehler: Route existiert nicht */}
<Link href="/users/123/profile" /> // users statt user
</nav>
)
}
Bei href="/ listet die IDE alle gültigen Routen. Tippfehler werden sofort rot markiert.
Beim ersten Mal dachte ich nur: endlich.
Einschränkungen
- Nur App Router: Mit
pagesfunktioniert es nicht - Dynamische Parameter manuell: Bei
/blog/[slug]müssen Sie slug selbst setzen - Query-Parameter ungeprüft:
tabin/user?tab=settingswird nicht typisiert
Es verhindert Pfad-Tippfehler; Parameterwerte bleiben Ihre Verantwortung.
Drittanbieter: nextjs-routes
Bei pages oder wenn auch Query-Parameter typisiert sein sollen: nextjs-routes.
Installation und Setup:
npm install nextjs-routes
In next.config.ts:
const nextRoutes = require('nextjs-routes/config')
const nextConfig = nextRoutes({
// Ihre bestehende Next.js-Konfiguration
})
export default nextConfig
Verwendung:
Die Bibliothek liefert eine route-Funktion:
import { route } from 'nextjs-routes'
// Typsicheres Route-Objekt
const profileRoute = route({
pathname: '/user/[id]/profile',
query: {
id: '123',
tab: 'settings', // Query-Parameter ebenfalls typisiert
}
})
router.push(profileRoute) // Vollständig typsicher
// Falscher Pfad
const wrongRoute = route({
pathname: '/users/[id]/profile', // ❌ TypeScript-Fehler: Pfad existiert nicht
})
Vorteile gegenüber der eingebauten Lösung:
- Unterstützt
pages - Query-Parameter typisiert
- Routen als Objekt statt String-Konkatenation
Nachteile: zusätzliche Dependency; Typen werden bei Routenänderungen neu generiert (automatisch).
Typen für Routenparameter
Bei app/blog/[slug]/page.tsx: Welcher Typ hat slug?
Next.js generiert params-Typen:
// app/blog/[slug]/page.tsx
export default function BlogPost({
params,
}: {
params: { slug: string }
}) {
return <h1>Artikel: {params.slug}</h1>
}
Problem: slug ist nur string. Für strengere Regeln – z. B. nur Kleinbuchstaben, Ziffern und Bindestriche – Laufzeitvalidierung mit zod:
import { z } from 'zod'
const slugSchema = z.string().regex(/^[a-z0-9-]+$/)
export default function BlogPost({
params,
}: {
params: { slug: string }
}) {
const validatedSlug = slugSchema.parse(params.slug)
return <h1>Artikel: {validatedSlug}</h1>
}
Ungültige slugs (Großbuchstaben, Sonderzeichen) werfen einen Fehler.
Besonders nützlich bei API-Routen: Eingaben von außen sollten Sie validieren, bevor es in Produktion knallt.
Typisierte Umgebungsvariablen – any endgültig loswerden
Woher das Problem kommt
TypeScript unterstützt Umgebungsvariablen standardmäßig schwach.
const apiKey = process.env.API_KEY
Typ von apiKey: string | undefined. Immerhin.
Häufiger:
const apiUrl = process.env.NEXT_PUBLIC_API_URL
console.log(apiUrl.toUpperCase()) // Laufzeitfehler: apiUrl is undefined
TypeScript schweigt; erst zur Laufzeit fehlt die Variable.
Tippfehler im Namen erkennt TypeScript ebenfalls nicht:
const key = process.env.API_SECRE // T fehlt
// TypeScript: string | undefined, alles ok
Mit TypeScript und trotzdem manuelle Namensprüfung – wie JavaScript.
T3 Env (empfohlen)
Die gängige Lösung: T3 Env – Typprüfung und Laufzeitvalidierung zusammen.
Installation:
npm install @t3-oss/env-nextjs zod
Konfiguration:
Im Projektroot env.mjs (oder env.ts):
import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod"
export const env = createEnv({
// Server-Variablen (nicht im Client)
server: {
DATABASE_URL: z.string().url(),
API_SECRET: z.string().min(32),
SMTP_HOST: z.string().min(1),
},
// Client-Variablen (NEXT_PUBLIC_ prefix)
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_ANALYTICS_ID: z.string().optional(),
},
// Mapping zur Laufzeit
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
API_SECRET: process.env.API_SECRET,
SMTP_HOST: process.env.SMTP_HOST,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_ANALYTICS_ID: process.env.NEXT_PUBLIC_ANALYTICS_ID,
},
})
Verwendung:
import { env } from './env.mjs'
// ✅ Typsicher, Autovervollständigung
const dbUrl = env.DATABASE_URL // string
const appUrl = env.NEXT_PUBLIC_APP_URL // string
// ❌ Tippfehler
const wrong = env.DATABASE_UR
// ❌ Client darf Server-Variable nicht lesen
'use client'
const secret = env.API_SECRET // Kompilierfehler
Vorteile:
- Validierung beim Start: Fehlende oder falsche Variablen fallen sofort auf
- Präzise Typen: nicht mehr nur
string | undefined - Kein Leak: Client-Zugriff auf Server-Secrets → Kompilierfehler
Vor T3 Env hing die Testumgebung oft, weil eine Variable fehlte – erst Logs durchsuchen. Jetzt scheitert der Start sofort mit klarer Meldung.
Eigene Typdeklarationen
Ohne T3 Env oder in kleinen Projekten: ProcessEnv erweitern:
// env.d.ts
namespace NodeJS {
interface ProcessEnv {
// Server
DATABASE_URL: string
API_SECRET: string
SMTP_HOST: string
// Client
NEXT_PUBLIC_APP_URL: string
NEXT_PUBLIC_ANALYTICS_ID?: string // optional mit ?
}
}
Dann kennt TypeScript die Typen:
const dbUrl = process.env.DATABASE_URL // string
const apiSecret = process.env.API_SECRET // string
// ❌ TypeScript-Fehler
const wrong = process.env.DATABASE_UR // Property 'DATABASE_UR' does not exist
Nachteile:
- Keine Laufzeitvalidierung
- Kein Schutz vor Client-Zugriff auf Server-Variablen
- Manuelle Pflege der Definitionen
Für kleine Projekte ok – wer TypeScript ernst nimmt, sollte T3 Env in Betracht ziehen.
Strict mode in der Praxis
Typenprobleme bei Drittanbieter-Bibliotheken
Manchmal liegt es nicht am eigenen Code, sondern an fehlenden oder fehlerhaften Typdefinitionen.
Fall 1: Keine Typen
import oldLib from 'some-old-lib' // any
Zuerst prüfen, ob @types/some-old-lib existiert:
npm install -D @types/some-old-lib
Falls nicht: types/some-old-lib.d.ts anlegen:
declare module 'some-old-lib' {
export function doSomething(param: string): number
export default someOldLib
}
Fall 2: Falsche @types
Bei schnell iterierenden Bibliotheken weichen Typen von der API ab. Temporär mit Assertion:
import { someFunction } from 'buggy-lib'
// Typ sagt string, API liefert number
const result = someFunction() as number
Langfristig: Issue oder PR beim Maintainer.
skipLibCheck – ja oder nein?
Empfehlung: aktivieren.
Typfehler in node_modules können Sie nicht fixen; sie verlangsamen nur den Build. Fokus auf eigenen Code.
Typische any-Fallen und Gegenmaßnahmen
Auch mit strict mode entweichen Typen manchmal zu any.
Fall 1: Event-Handler
// ❌ Schlecht
const handleSubmit = (e: any) => {
e.preventDefault()
}
// ✅ Richtig
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
// e.currentTarget voll typisiert
}
Häufige Event-Typen:
React.MouseEvent<HTMLButtonElement>React.ChangeEvent<HTMLInputElement>React.KeyboardEvent<HTMLDivElement>
Fall 2: API-Antworten
// ❌ Schlecht
const res = await fetch('/api/user')
const data = await res.json() // any
// ✅ Option 1: Interface
interface User {
id: string
name: string
email: string
}
const data: User = await res.json()
// ✅ Option 2: zod (empfohlen)
import { z } from 'zod'
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
const data = UserSchema.parse(await res.json())
zod liefert Typen und Laufzeitvalidierung – Schema-Änderungen im Backend fallen sofort auf.
Fall 3: Dynamischer Import
// ❌ Schlecht
const module = await import('./utils') // any
// ✅ Besser
const module = await import('./utils') as typeof import('./utils')
Oder destrukturiert:
const { formatDate } = await import('./utils') // Typen werden inferiert
Utility Types für effizientere Entwicklung
TypeScript bringt viele Utility Types mit.
Pick: Teilmenge der Properties
interface User {
id: string
name: string
email: string
password: string
createdAt: Date
}
type PublicUser = Pick<User, 'id' | 'name' | 'email'>
// { id: string; name: string; email: string }
Omit: Properties ausschließen
type CreateUserInput = Omit<User, 'id' | 'createdAt'>
Partial: alles optional
type UpdateUserInput = Partial<User>
Required: alles Pflicht
type RequiredUser = Required<Partial<User>>
Eigene Utility Types
type PartialString<T> = {
[K in keyof T]: T[K] extends string ? T[K] | undefined : T[K]
}
Am Anfang wirken Utility Types abstrakt – bei komplexen Objekttypen sparen sie viel Boilerplate.
Fazit
Zurück zum Bug von Anfang an.
Mit typedRoutes wäre der Routen-Tippfehler nicht live gegangen. Mit T3 Env wäre eine fehlende Umgebungsvariable beim Start aufgefallen. Mit sauberem strict mode hätte TypeScript implizites any früh gemeldet.
Typsicherheit soll nicht nerven – sie verschiebt Bugs von der Laufzeit in die Entwicklung. Lieber rote Wellenlinien in der IDE als weiße Bildschirme für Nutzer.
Kernpunkte:
- tsconfig: strict, incremental, paths, Next.js-Plugin
- Typsichere Routen: typedRoutes in Next.js 13+ oder nextjs-routes
- Umgebungsvariablen: T3 Env für Typen + Laufzeitvalidierung
- Strict in der Praxis: schrittweise aktivieren, Drittanbieter-Typen handhaben, any-Fallen vermeiden
Am Anfang wirkt Konfiguration und Annotation aufwendig. Wenn Sie sich an präzise IDE-Hinweise und frühes Erkennen von Problemen gewöhnt haben, wollen Sie nicht mehr „nacktes” JavaScript.
Öffnen Sie Ihre tsconfig.json und setzen Sie strict auf true. Je mehr rote Wellenlinien, desto mehr potenzielle Bugs finden Sie – und das ist gut so.
FAQ
Verlangsamt strict mode die Projektkompilierung?
Wie aktiviere ich strict mode sicher in einem bestehenden Projekt?
Was ist der Unterschied zwischen T3 Env und manuell definierten ProcessEnv-Typen?
Unterstützt typedRoutes von Next.js das pages-Verzeichnis?
Entstehen Sicherheitsrisiken, wenn skipLibCheck aktiviert ist?
11 Min. Lesezeit · Veröffentlicht am: 6. Jan. 2026 · Aktualisiert am: 14. Juli 2026
Next.js Komplettleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Next.js Sitemap und robots.txt: Leitfaden für schnelle Suchmaschinen-Indexierung
Vollständige Anleitung zu Sitemap und robots.txt in Next.js: drei Generierungsvarianten, typische Fehler, Integration mit Google Search Console – damit neue Websites schneller indexiert werden.
Teil 29 von 51
Nächster
Next.js Engineering-Setup: ESLint + Prettier + Husky in einem Rutsch
Wurde Ihr PR am Freitagabend wegen Formatproblemen abgelehnt? Verursacht ein uneinheitlicher Code-Stil sinnlose Merge-Konflikte? Schritt für Schritt konfigurieren Sie ESLint, Prettier und Husky für automatische Checks und Formatierung – effizientere Teamarbeit inklusive.
Teil 31 von 51
Ähnliche Beiträge
Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick

Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick
Next.js 15 in der Praxis: Wie ich an einem Wochenende ein produktionsreifes Blog-System aufgebaut habe


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen