Zum Inhalt springen

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.

4 Min. Lesezeit
Code-Editor mit TypeScript-Type-Narrowing für unknown mittels Type Guards

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.

tstypescript
// ❌ 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 undefined

Der 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.

tstypescript
// ✅ 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:

tstypescript
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:

tstypescript
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:

OrtTypische any-NutzungErsatz mit unknown
catch-Blöckecatch (err: any)catch (err: unknown), mit instanceof Error eingrenzen
JSON.parse-Ergebnisseimpliziter any-Rückgabetyp: unknown annotieren, mit Schema validieren
Externe API-Antwortenfetch(...).then(r => r.json()) typisiert als anyRü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:

tstypescript
// ❌ 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:

tstypescript
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 any eine bewusste, temporäre Zwischenstation auf dem Weg zu strengeren Typen ist.
  • Generic Constraints in seltenen Fällen, in denen unknown unnö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

  1. any deaktiviert die Typprüfung für einen Wert und alles, was davon abgeleitet ist – Fehler zeigen sich zur Laufzeit statt zur Compile-Zeit.
  2. unknown akzeptiert jeden Wert, verlangt aber Narrowing vor der Nutzung – genau das macht es sicher.
  3. Wiederverwendbare Type Guards (value is Type) halten das Narrowing kompakt, statt Prüfungen inline zu wiederholen.
  4. 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.
  5. catch-Blöcke, JSON.parse-Ergebnisse und externe API-Antworten sind die drei Stellen, an denen sich any am häufigsten einschleicht – ersetze alle drei durch unknown plus einen Narrowing-Schritt.
  6. Hebe dir any für dokumentierte Ausnahmen auf – untypisierten Third-Party-Code oder einen bewussten Migrationsschritt – nicht als Standard, wenn das Schreiben eines Type Guards gerade unbequem erscheint.
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX