Es gibt unzählige Artikel, die erklären, was TypeScript ist. Dieser Beitrag stellt eine andere Frage:
Du hast strict: true aktiviert, any nicht angefasst, der Compiler läuft durch – ist dein Programm damit korrekt?
Nein, ist es nicht. Und zu wissen, wo es hakt, macht den Unterschied zwischen einer echten Nutzung des Typsystems und blindem Vertrauen darauf.
Die grundlegende Wahrheit: Typen gibt es zur Laufzeit nicht
Beim Kompilieren von TypeScript werden Typen gelöscht. Was tatsächlich im Browser oder in Node landet, ist reines JavaScript; keine einzige Typprüfung überlebt bis zur Laufzeit.
type User = { id: number; name: string };
function greet(user: User) {
return `Merhaba ${user.name}`;
}Nach dem Kompilieren bleibt von dieser Funktion nur Folgendes übrig:
function greet(user) {
return `Merhaba ${user.name}`;
}Wenn greet zur Laufzeit also null bekommt, hält nichts es auf. Die Garantie von TypeScript lautet: Du kannst aus deinem eigenen Code heraus keinen ungültigen Aufruf machen. Alles, was von außerhalb deiner Codebasis hereinkommt – API-Antworten, Formulardaten, localStorage, Umgebungsvariablen – fällt komplett aus dieser Garantie heraus.
Die fünf folgenden Lücken ergeben sich alle aus genau dieser Realität.
Lücke 1: as ist keine Prüfung, sondern eine Behauptung
Das ist die häufigste und gefährlichste Lücke.
const res = await fetch("/api/user");
const user = (await res.json()) as User;
console.log(user.name.toUpperCase());Hier findet keinerlei Validierung statt. as User zu schreiben heißt nur: „Ich sage dir, das ist ein User.“ Liefert die API { error: "not found" }, bleibt der Compiler völlig still, und die Zeile fliegt zur Laufzeit mit Cannot read properties of undefined in die Luft.
Warnung: Wann solltest du as verwenden?
Nur, wenn du wirklich etwas weißt, das der Compiler unmöglich ableiten kann, zum Beispiel document.getElementById("x") as HTMLInputElement. Wenn du as auf Daten aus der Außenwelt anwendest, validierst du nichts – du glaubst es nur.
Lücke 2: JSON.parse behandelt alles als any
const raw = localStorage.getItem("settings");
const settings = JSON.parse(raw!); // any
settings.theme.colors.primary; // compiler is silentDer Rückgabetyp von JSON.parse ist any. Und any schaltet das Typsystem im Grunde ab: Alles, worauf du über any zugreifst, umgeht die Prüfungen ungeprüft, und dieses Verhalten breitet sich überallhin aus, wo der Wert zugewiesen wird.
Dasselbe gilt für fetch: res.json() gibt Promise<any> zurück.
Tipp: Eine kleine, aber wirksame Gewohnheit
Nimm das Ergebnis von JSON.parse als unknown entgegen. unknown akzeptiert ebenfalls jeden Wert, aber es zwingt dich, den Typ einzugrenzen, bevor du ihn verwendest:
const settings: unknown = JSON.parse(raw ?? "{}");
// settings.theme → compiler error, you need to validate firstLücke 3: Der !-Operator
Das Ausrufezeichen behauptet „das ist weder null noch undefined“ – wieder nur eine Behauptung.
const el = document.querySelector(".modal")!;
el.classList.add("open"); // blows up at runtime if the element doesn't exist
const user = users.find((u) => u.id === id)!;Die zweite Zeile ist besonders tückisch: Findet find nichts, gibt es undefined zurück, aber ! bringt den Compiler zum Schweigen. Richtig ist, die Prüfung explizit zu behandeln:
const user = users.find((u) => u.id === id);
if (!user) {
return notFound();
}Lücke 4: strict fängt Array-Indexzugriffe nicht ab
Das habe ich selbst überprüft. Der folgende Code läuft unter strict: true ohne einen einzigen Fehler durch:
const arr: string[] = [];
const first = arr[0];
console.log(first.length); // at runtime: Cannot read properties of undefinedTypeScript geht davon aus, dass arr[0] den Typ string hat, obwohl das Array komplett leer ist. Genau dasselbe Problem gilt für Property-Zugriffe auf Record<string, T>.
Die Lösung steckt nicht in strict; dafür brauchst du ein explizites Flag:
Beispiel: tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}Jetzt wirft derselbe Code einen Fehler: 'first' is possibly 'undefined'.
Wenn du dieses Flag aktivierst, siehst du in bestehendem Code womöglich eine Flut von Fehlern – aber jeder einzelne davon weist auf ein echtes Laufzeitrisiko hin.
Erklärung: Weitere Flags, die nicht in strict enthalten sind
strict: true ist ein Bündel, enthält aber nicht alles. Diese Flags musst du separat aktivieren:
noUncheckedIndexedAccess— ergänzt bei Index-Zugriffen auf Arrays und Objekte| undefinedexactOptionalPropertyTypes— verhindert, dass du Properties vom Typ{ a?: string }explizitundefinedzuweistnoImplicitOverride— erzwingtoverridebei KlassenmethodennoPropertyAccessFromIndexSignature— verhindert Zugriffe per Punktnotation auf Index Signatures
Die ersten beiden solltest du in jedem neuen Projekt von Anfang an einschalten; sie später zu aktivieren, wird zu einer vollständigen Migration.
Lücke 5: process.env und Konfiguration
const dbUrl = process.env.DATABASE_URL; // string | undefined
const client = new Client({ url: dbUrl! }); // assertion againEine Umgebungsvariable ist kein Code, sondern ein Deployment-Detail. Ist eine Variable auf dem Server nicht gesetzt, tritt der Fehler an der schlimmstmöglichen Stelle auf: nicht beim Start, sondern in dem Moment, in dem genau dieser Codepfad zum ersten Mal läuft. Das richtige Muster ist, einmalig beim Start zu validieren:
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Eksik ortam değişkeni: ${name}`);
}
return value;
}
export const env = {
databaseUrl: requireEnv("DATABASE_URL"),
resendApiKey: requireEnv("RESEND_API_KEY"),
};Jetzt weigert sich die App schlicht zu starten, wenn Konfiguration fehlt – sie scheitert früh, eindeutig und zum richtigen Zeitpunkt.
Die Lücken an der Grenze schließen
Alle fünf Lücken haben einen gemeinsamen Nenner: Sie entstehen genau an der Grenze, an der externe Daten ins System gelangen. Deshalb gibt es nur eine echte Lösung: an den Grenzen zur Laufzeit validieren und im Inneren deinen Typen vertrauen.
Zod löst das in einem Schritt – es validiert die Laufzeitdaten und leitet den TypeScript-Typ automatisch aus dem Schema ab, sodass du Typen nie doppelt deklarierst:
Beispiel: Validierung an der Grenze
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// Type is inferred from the schema — no need to write `type User` separately
type User = z.infer<typeof UserSchema>;
export async function getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const json: unknown = await res.json();
// This actually validates at runtime; it's not an assertion like `as`
return UserSchema.parse(json);
}Ab diesem Punkt weißt du, dass user.name ein String ist; du rätst nicht. Genau derselbe Ansatz gilt für Formulardaten, Webhook-Payloads und localStorage.
Passend dazu: Formularvalidierung in Next.js: Client- und Server-seitig
Zwei veraltete Gewohnheiten
Hör auf, React.FC zu verwenden. Eine Zeit lang war es Standard, aber heute ist es völlig unnötig. Props direkt zu typisieren ist sauberer und harmoniert deutlich besser mit generischen Komponenten:
// Old
const Button: React.FC<ButtonProps> = ({ children }) => { /* ... */ };
// Now
function Button({ children }: ButtonProps) { /* ... */ }Nimm statt enum lieber as const. Beim Kompilieren erzeugt enum echten Laufzeitcode – Typen werden entfernt, Enums bleiben. Ein Union Type dagegen erzeugt überhaupt nichts:
// Generates an object at runtime
enum LogLevel { DEBUG = "debug", INFO = "info" }
// Generates nothing, does the exact same job
const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
type LogLevel = (typeof LOG_LEVELS)[number];Warnung: Vorsicht bei const enum
Vielleicht liest du den Rat, „const enum sei performanter“, aber es gibt einen gewichtigen Haken: Ist isolatedModules aktiviert – was Next.js erzwingt –, löst der Zugriff auf ein ambientes const enum in einer .d.ts einen Build-Fehler aus:
TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled.
Ein const enum, das in deiner eigenen Datei definiert und importiert wird, funktioniert problemlos; ein ambientes aus einem Drittanbieter-Paket scheitert. Kennst du diesen Unterschied nicht, kostet dich das Debugging Stunden.
Zusammenfassung
Zusammenfassung: Checkliste
- Typen gibt es zur Laufzeit nicht; Garantien gelten nur innerhalb deines eigenen Codes
asund!sind Behauptungen; wendest du sie auf externe Eingaben an, hat keine Validierung stattgefundenJSON.parseundres.json()gebenanyzurück – nimm sie alsunknownentgegen, um das Eingrenzen zu erzwingenstrictfängt Array-Indexzugriffe nicht ab; aktivierenoUncheckedIndexedAccessseparat- Validiere Umgebungsvariablen einmalig beim Start
- Verlege die Laufzeitvalidierung an die Grenzen (mit Zod) und vertraue intern deinen Typen
- Überdenke veraltete Gewohnheiten rund um
React.FCundenum
TypeScript ist keine Engine zur formalen Verifikation, sondern ein Kommunikationswerkzeug. Du hältst damit in deinem Code fest, was du weißt – aber wenn du es anlügst, glaubt es dir stillschweigend. Das Schwierige ist nicht, Typen zu schreiben, sondern sicherzustellen, dass du den Compiler nicht anlügst.
Interaktive Roadmap & Tool
Junior Frontend Developer 2026
Was du wirklich wissen musst, um 2026 einen Junior-Frontend-Job zu bekommen, und was warten kann – abgeleitet aus echten Stellenanzeigen.
SQL oder NoSQL?
Beantworte sechs Fragen, und ich sage dir, welche Art von Datenbank zu deinem Projekt passt und warum.