Schluss mit any: unknown in TypeScript sicher eingrenzen
Warum unknown die typsichere Alternative zu any ist – und welche Narrowing-Patterns es im Alltag praktikabel machen.

Jede Codebasis, die ich übernommen habe, zeigt dasselbe Muster: Ein grep nach any liefert Hunderte Treffer, und die Hälfte davon sitzt genau an den Stellen, an denen später ein Bug in Produktion auftauchte. any ist kein Typ – es ist eine Anweisung an den Compiler, mit dem Prüfen aufzuhören. unknown bietet dieselbe Flexibilität an der Grenze deines Programms, ohne das Sicherheitsnetz überall stromabwärts aufzugeben. Der Grund, warum Teams es nicht öfter nutzen, ist schlicht, dass ihnen niemand gezeigt hat, wie man es eingrenzt, ohne eine Wand aus if-Anweisungen zu schreiben. Genau das ist die eigentliche Fähigkeit.
Was any dich wirklich kostet
any ist ansteckend. Sobald ein Wert als any typisiert ist, sind auch jeder Property-Zugriff, jeder Funktionsaufruf und jede arithmetische Operation darauf any – und TypeScript reicht das bereitwillig durch die gesamte Aufrufkette weiter, ohne einen einzigen Fehler zu melden.
// ❌ any silently disables checking for everything downstream
function parseConfig(raw: any) {
return {
port: raw.port,
// typo goes unnoticed — "confg.hsot" compiles fine
host: raw.hsot,
timeout: raw.timeout * 1000,
};
}
const config = parseConfig(JSON.parse(fileContents));
config.host.toUpperCase(); // runtime crash: host is undefinedDer Compiler hat hier null Warnungen ausgegeben. raw.hsot ist ein Tippfehler, raw.timeout * 1000 setzt eine Zahl voraus, wo auch ein String stehen könnte, und config.host bleibt any bis zur Aufrufstelle, an der es schließlich crasht – zur Laufzeit, vor den Augen eines Nutzers, drei Dateien entfernt von der Stelle, an der der Fehler tatsächlich gemacht wurde.
unknown als typsichere Grenze
unknown ist das typsichere Gegenstück, beschrieben im Narrowing-Kapitel des TypeScript Handbook: Du kannst ihm alles zuweisen, aber du kannst nichts damit tun, bevor du nicht bewiesen hast, was es ist.
// ✅ unknown forces you to prove the shape before using it
function parseConfig(raw: unknown): { port: number; host: string } {
if (
typeof raw !== "object" ||
raw === null ||
!("port" in raw) ||
!("host" in raw)
) {
throw new Error("Invalid config shape");
}
const { port, host } = raw as { port: unknown; host: unknown };
if (typeof port !== "number" || typeof host !== "string") {
throw new Error("Invalid config field types");
}
return { port, host };
}Zugegeben, das ist umständlicher. Aber der Tippfehler aus dem vorigen Beispiel ist jetzt ein Compile-Fehler und kein Anruf um 2 Uhr nachts. Dieser Tausch – ein paar zusätzliche Zeilen an der Grenze gegen Korrektheit überall stromabwärts – lohnt sich fast immer.
Narrowing-Techniken, die sich nicht wie Fleißarbeit anfühlen
Die Reibung mit unknown verschwindet, sobald du ein kleines Set an Narrowing-Patterns hast, das du wiederverwendest, statt es bei jeder Aufrufstelle neu zu erfinden.
Type Guards sind die sauberste Lösung, wenn die Prüfung wiederverwendbar sein soll:
interface User {
id: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
typeof (value as Record<string, unknown>).id === "string" &&
typeof (value as Record<string, unknown>).email === "string"
);
}
function greet(value: unknown) {
if (!isUser(value)) {
throw new Error("Expected a User");
}
// value is narrowed to User from this line onward
console.log(`Hello, ${value.email}`);
}Laufzeit-Validierungsbibliotheken machen den manuellen Guard komplett überflüssig, sobald die Shapes so komplex werden, dass handgeschriebene Prüfungen selbst fehleranfällig werden. Zod ist das Muster, zu dem ich am häufigsten greife:
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
function parseUser(payload: unknown): User {
// throws with a descriptive error if payload doesn't match
return UserSchema.parse(payload);
}Das ist dieselbe Idee wie beim manuellen Type Guard, nur dass das Schema gleichzeitig die einzige Quelle der Wahrheit für die Laufzeitprüfung und den Compile-Time-Typ ist – kein Risiko, dass die beiden auseinanderdriften.
Wo unknown in echtem Code tatsächlich auftaucht
Drei Stellen machen den Großteil des any aus, das du in einer bestehenden Codebasis findest, und für alle drei gibt es einen direkten Ersatz auf Basis von unknown:
| Ort | Typische any-Nutzung | Ersatz mit unknown |
|---|---|---|
catch-Blöcke | catch (err: any) | catch (err: unknown), mit instanceof Error eingrenzen |
JSON.parse-Ergebnisse | impliziter any-Rückgabetyp | : unknown annotieren, mit Schema validieren |
| Externe API-Antworten | fetch(...).then(r => r.json()) typisiert als any | Rückgabewert des Fetch-Wrappers als unknown typisieren |
TypeScript 4.4 hat die erste Zeile obsolet gemacht, indem es erlaubt, gefangene Fehler als unknown zu typisieren – die Release Notes erklären, warum any davor der Standard war und warum das keine gute Wahl war:
// ❌ err is any — every property access is unchecked
try {
await saveUser(user);
} catch (err: any) {
console.error(err.message); // works, but also compiles if err is a string
}
// ✅ err is unknown — you have to prove it's an Error first
try {
await saveUser(user);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error(message);
}JSON.parse ist dieselbe Geschichte im Kleinen: Sein Rückgabetyp ist in den Typings der Standardbibliothek standardmäßig any, was bedeutet, dass du in dem Moment, in dem du es aufrufst, stillschweigend jedes Problem wieder einführst, das unknown eigentlich lösen sollte. Wickle es einmal ein:
function parseJson(text: string): unknown {
return JSON.parse(text);
}und jede Aufrufstelle ist gezwungen, zu validieren, bevor sie das Ergebnis nutzen kann.
Wann any noch die richtige Wahl ist
unknown ist keine Regel, die man ausnahmslos überall anwenden sollte. any hat weiterhin ein paar legitime Einsatzzwecke:
- Third-Party-Bibliotheken mit fehlenden oder kaputten Typdefinitionen, als dokumentierte, isolierte Ausnahme – nicht verteilt über deinen eigenen Code.
- Schrittweise Migration einer großen JavaScript-Codebasis, bei der
anyeine bewusste, temporäre Zwischenstation auf dem Weg zu strengeren Typen ist. - Generic Constraints in seltenen Fällen, in denen
unknownunnötige Casts in Code erzwingen würde, der durch seine Konstruktion nachweislich sicher ist (etwa ein gut getestetes internes Utility).
Der Unterschied zu dem parseConfig-Beispiel von vorhin liegt in der Absicht. any als bewusste, eng begrenzte Entscheidung ist völlig in Ordnung. any als Standard, weil niemand einen Type Guard schreiben wollte, ist da, wo Bugs herkommen.
Key Takeaways
anydeaktiviert die Typprüfung für einen Wert und alles, was davon abgeleitet ist – Fehler zeigen sich zur Laufzeit statt zur Compile-Zeit.unknownakzeptiert jeden Wert, verlangt aber Narrowing vor der Nutzung – genau das macht es sicher.- Wiederverwendbare Type Guards (
value is Type) halten das Narrowing kompakt, statt Prüfungen inline zu wiederholen. - Schema-basierte Validierung (Zod oder Ähnliches) ist das richtige Werkzeug, sobald Shapes so komplex werden, dass handgeschriebene Guards Gefahr laufen, von der Realität abzudriften.
catch-Blöcke,JSON.parse-Ergebnisse und externe API-Antworten sind die drei Stellen, an denen sichanyam häufigsten einschleicht – ersetze alle drei durchunknownplus einen Narrowing-Schritt.- Hebe dir
anyfür dokumentierte Ausnahmen auf – untypisierten Third-Party-Code oder einen bewussten Migrationsschritt – nicht als Standard, wenn das Schreiben eines Type Guards gerade unbequem erscheint.


