Design wechseln

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

Easton editorial illustration: server-client bridge

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

60%
Schnellere 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

  1. Nur App Router: Mit pages funktioniert es nicht
  2. Dynamische Parameter manuell: Bei /blog/[slug] müssen Sie slug selbst setzen
  3. Query-Parameter ungeprüft: tab in /user?tab=settings wird 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 EnvTypprü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:

  1. Validierung beim Start: Fehlende oder falsche Variablen fallen sofort auf
  2. Präzise Typen: nicht mehr nur string | undefined
  3. 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:

  1. tsconfig: strict, incremental, paths, Next.js-Plugin
  2. Typsichere Routen: typedRoutes in Next.js 13+ oder nextjs-routes
  3. Umgebungsvariablen: T3 Env für Typen + Laufzeitvalidierung
  4. 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?
Nein. Strict mode erhöht nur die Strenge der Typprüfung und beeinflusst die Kompiliergeschwindigkeit kaum. Mit incremental kann die Kompilierung in großen Projekten sogar um 30–50 % schneller werden.
Wie aktiviere ich strict mode sicher in einem bestehenden Projekt?
Schrittweise vorgehen: strict in tsconfig.json aktivieren, Dateien, die noch nicht angepasst werden können, mit @ts-expect-error markieren, neuen Code strikt halten und alten Code nach und nach refaktorieren. Alternativ strict pro Modul aktivieren.
Was ist der Unterschied zwischen T3 Env und manuell definierten ProcessEnv-Typen?
T3 Env bietet Laufzeitvalidierung: Beim Start prüft die App, ob Umgebungsvariablen fehlen oder das Format falsch ist, und verhindert Client-Zugriff auf Server-Variablen. Manuelle Typdefinitionen prüfen nur zur Kompilierzeit – ohne Laufzeitschutz.
Unterstützt typedRoutes von Next.js das pages-Verzeichnis?
Nein. typedRoutes ist eine experimentelle Funktion in Next.js 13+ für den App Router und gilt nur für das app-Verzeichnis. Bei pages empfiehlt sich die Bibliothek nextjs-routes.
Entstehen Sicherheitsrisiken, wenn skipLibCheck aktiviert ist?
Nein. skipLibCheck überspringt nur die Typprüfung in node_modules – Ihr eigener Code wird weiterhin streng geprüft. Typfehler in Drittanbieter-Bibliotheken können Sie ohnehin nicht beheben; das Überspringen beschleunigt die Kompilierung und hält den Fokus auf dem eigenen Code.

11 Min. Lesezeit · Veröffentlicht am: 6. Jan. 2026 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog