Die Ersetzung von TypeScript’s `any`: Sechs typsichere Muster für häufige Fälle
Erlernen Sie praktische, typsichere Alternativen zu TypeScripts `any` – einschließlich unknown, Generics, diskriminierte Unionen und ausführlichen Überprüfungen – zur Handhabung unvorhersehbarer Daten.
Quellen: Stop Using ‘any’ in TypeScript — Here’s the Right Fix Every Time
Eine kleine Disziplinänderung, die Sie vor Überraschungen in der Produktion schützt
Jeder TypeScript-Entwickler war schon einmal in dieser Situation. Es ist spät am Freitagnachmittag, ein seltsamer Compilerfehler hält Ihren Pull Request auf, und nach zwanzig Minuten Kampf mit verschachtelten Generics und Union-Typen möchten Sie nur noch den Code veröffentlichen und Ihren Laptop zuklappen.
Deshalb greifen Sie zu den drei Buchstaben, die alles – und doch auch nichts – lösen:
const payload: any = response.data;
Die rote Unterline verschwindet. Der Build wird grün angezeigt. Alles scheint in Ordnung zu sein.
Aber das ist nicht in Ordnung. Das Schreiben von any löst kein Typisierungsproblem – es sagt TypeScript lediglich, aufzuhören, darauf zu achten. Man deaktiviert damit genau das Sicherheitsnetz, das ursprünglich dazu geführt hat, TypeScript zu wählen. Jedes Mal, wenn any auftaucht, entsteht ein Loch in der Typabdeckung, und dieses Loch breitet sich leise auf jede Funktion und jeden Variablen aus, die damit in Berührung kommt.
Die gute Nachricht ist, dass man es fast nie wirklich benötigt. Im Folgenden wird erklärt, warum any so viel Schaden anrichtet, gefolgt von bewährten Ersatzlösungen für die Situationen, in denen Entwickler am häufigsten darauf zurückgreifen.
Was any Tatsächlich Bedeutet (und Warum Es Nachteile Bringt)
Viele Entwickler gehen davon aus, dass any einfach bedeutet „der Typ dieses Wertes ist noch nicht bekannt.“
Was tatsächlich an den Compiler übermittelt wird, ist: „Hören Sie auf, diese Variable zu überprüfen, und vertrauen Sie darauf, dass alles, was ich danach mit ihr mache, korrekt ist.“
let user: any = { name: "Alice" };
// TypeScript allows all of these without a single warning:
user.authenticate(); // Runtime crash: user.authenticate is not a function
user.address.postalCode; // Runtime crash: Cannot read properties of undefined
user(); // Runtime crash: user is not a function
Was die Situation noch verschlimmert, sind any-Typen. Sobald der Rückgabetyp einer Funktion any ist, werden auch alle Variablen, die diesen Rückgabewert speichern, zu any. Wenn dieser Wert an eine andere Funktion weitergegeben wird, setzt sich der Verlust an Sicherheit fort. Schon bald sind große Teile Ihrer Codebasis im Grunde genommen reiner JavaScript-Code mit einer .ts-Erweiterung.
In den folgenden Abschnitten werden die richtigen Alternativen für jeden gängigen Fall erläutert, in dem any oft auftaucht.
Lösung 1: Wenn der Typ tatsächlich noch nicht bekannt ist (unknown)
Die Situation: Sie ziehen Daten aus einer externen API ab, parsen eine aus dem lokalen Speicher geladene JSON-String oder verarbeiten einen rohen Payload von einem webhook. In diesen Fällen ist es vor dem eigentlichen Ausführen des Programms nicht möglich, die genaue Struktur der Daten zu kennen.
Der problematische Ansatz:
function parseApiResponse(jsonString: string): any {
return JSON.parse(jsonString);
}
const data = parseApiResponse('{"id": 101}');
console.log(data.profile.avatar); // Compiles fine, crashes immediately if profile is missing
Die richtige Lösung: Verwenden Sie unknown in Kombination mit einem Type Guard.
unknown funktioniert wie ein sichererer „Bruder“ von any. Es teilt dem Compiler mit: „Die Typenart dieses Wertes ist noch nicht festgelegt, aber es ist nicht zulässig, Eigenschaften darauf zuzugreifen, Methoden aufzurufen oder sonstwie damit zu arbeiten, bis geklärt ist, was es tatsächlich ist.“
interface UserProfile {
id: number;
name: string;
}
// Custom type guard
function isUserProfile(value: unknown): value is UserProfile {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof (value as Record<string, unknown>).id === "number" &&
"name" in value &&
typeof (value as Record<string, unknown>).name === "string"
);
}
function parseApiResponse(jsonString: string): unknown {
return JSON.parse(jsonString);
}
const rawData = parseApiResponse('{"id": 101, "name": "Sarah"}');
// rawData.name; // Error: Object is of type 'unknown'.
if (isUserProfile(rawData)) {
// Inside this block, TypeScript knows rawData is definitely UserProfile
console.log(rawData.name); // Fully typed, autocompleted, and safe!
}
Falls das Schreiben von Überprüfungen für komplexe Datenmengen zu viel Aufwand erscheint, können Sie eine Laufzeit-Validierungsbibliothek wie Zod oder Valibot verwenden. Diese Bibliotheken validieren unbekannte Eingaben und ergeben für Sie in einem einzigen Schritt den entsprechenden TypeScript-Typ.
Lösung 2: Wenn eine Funktion mehrere Typen unterstützen muss (Generics)
Die Situation: Sie entwickeln ein wiederverwendbares Hilfsprogramm – etwa einen Caching-Wrapper, eine Array-Hilfsmethode oder eine Methode in einem API-Client – das mit vielen verschiedenen Datenschemata arbeiten soll.
Der problematische Ansatz:
function getFirstItem(items: any[]): any {
return items[0];
}
const numbers = [1, 2, 3];
const firstNum = getFirstItem(numbers); // firstNum is typed as 'any'
firstNum.toUpperCase(); // Compiles! Crashes at runtime because it's a number.
Die richtige Lösung: Wenden Sie Generics (die <T>-Syntax) an.
Generics fungieren als Platzhalter für Typen statt für Werte. Anstatt die Typinformationen wie any zu ignorieren, merkt sich ein Generic den vom Aufrufer bereitgestellten Typ und behält ihn bis zur Rückgabe des Ergebnisses der Funktion bei.
function getFirstItem<T>(items: T[]): T | undefined {
return items[0];
}
const numbers = [1, 2, 3];
const firstNum = getFirstItem(numbers);
// TypeScript automatically infers firstNum as: number | undefined
const names = ["Alice", "Bob"];
const firstName = getFirstItem(names);
// TypeScript automatically infers firstName as: string | undefined
Man kann außerdem einschränken, was ein Generic sein darf, indem man extends verwendet, um sicherzustellen, dass der Typ bestimmte erforderliche Eigenschaften enthält:
interface HasId {
id: string | number;
}
function printId<T extends HasId>(entity: T): void {
console.log(entity.id);
}
Fix 3: Fehlerbehandlung innerhalb von try / catch
Die Situation: Man schreibt Logik innerhalb eines catch-Blocks, um mit einem Fehler umzugehen.
Der problematische Ansatz:
try {
fetchUserData();
} catch (err: any) {
// Risky: In JavaScript, anything can be thrown (strings, numbers, null)
console.error(err.message.toUpperCase());
}
Das Risiko besteht darin, dass JavaScript es ermöglicht, buchstäblich alles auszuschließen – Strings, Zahlen, null, einfache Objekte, nicht nur Error-Instanzen. Wenn eine Bibliothek oder ein unerwarteter Fehler einen einfachen String auslöst, wie zum Beispiel throw „Network timeout“, dann wird err.message auf undefined setzen, und das Aufrufen von Methoden wie .toUpperCase() darauf kann einen neuen, unverarbeiteten Fehler direkt innerhalb des Codes auslösen, der eigentlich Fehler verarbeiten soll.
Die richtige Lösung: Den gefangenen Fehler als unknown typisieren und mithilfe von instanceof Error eingrenzen.
try {
fetchUserData();
} catch (err: unknown) {
if (err instanceof Error) {
// TypeScript safely knows 'err' has a .message property
console.error(`Error: ${err.message}`);
} else {
// Fallback for non-standard thrown objects
console.error("An unexpected error occurred", String(err));
}
}
Lösung 4: Dynamische Objekte und Schlüsselbasierte Abfragen
Die Situation: Man benötigt ein Objekt, das sich wie ein Speicher oder Wörterbuch verhält, wobei die Schlüssel erst während der Ausführung des Programms bekannt sind.
Der problematische Ansatz:
const userRolePermissions: any = {};
userRolePermissions["admin"] = ["read", "write", "delete"];
Die richtige Lösung: Verwenden Sie Record<Keys, Values> oder alternativ eine indizierte Signatur.
type Role = "admin" | "editor" | "viewer";
type Permission = "read" | "write" | "delete";
// A strictly defined key-value mapping
const rolePermissions: Record<Role, Permission[]> = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
// If the keys can be arbitrary dynamic strings:
const sessionCache: Record<string, unknown> = {};
Durch die Verwendung von Record<string, unknown> bleibt die Struktur des Objekts überprüfbar, während gleichzeitig jeder, der es verwendet, gezwungen wird, vor dem Zugriff zu überprüfen, was ein Wert tatsächlich ist.
Lösung 5: Angabe der Formwechsel (diskriminierte Unionen)
Die Situation: Ein Prozess durchläuft verschiedene Phasen – wie idle, loading, success und error – und jede Phase enthält ihre eigene Art von Daten.
Der problematische Ansatz:
interface RequestState {
status: any;
data?: any;
error?: any;
}
Das Modellieren mit einer einzigen, lockeren Schnittstelle bedeutet, dass jede Eigenschaft optional wird, wodurch man überall im Code ?.-Ketten verwenden muss. Noch schlimmer ist, dass nichts dagegen hilft, in einen sinnlosen Zustand zu geraten, in dem sowohl data als auch error gleichzeitig befüllt werden.
Die richtige Lösung: Nutzen Sie diskriminierte Unionen.
Weisen Sie jedem möglichen Zustand ein gemeinsames Literalfeld zu – üblicherweise status oder type genannt –, das TypeScript nutzen kann, um die Varianten voneinander zu unterscheiden:
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: Error };
function renderUI(state: RequestState) {
switch (state.status) {
case "idle":
return "Click start";
case "loading":
return "Loading...";
case "success":
// TypeScript automatically knows state.data exists here!
return `Loaded ${state.data.length} items`;
case "error":
// TypeScript automatically knows state.error exists here!
return `Failed: ${state.error.message}`;
}
}
Lösung 6: Ausführliche Überprüfung mit never
Die Situation: Sie möchten, dass der Compiler selbst darauf hinweist, wenn ein Teammitglied einen neuen Fall zu einem Union-Typ hinzufügt, aber vergisst, alle switch- oder if/else-Ketten, die ihn verarbeiten, zu aktualisieren.
Der problematische Ansatz: Auf eine allgemeine Lösung wie default: return any zurückzugreifen oder den zusätzlichen Fall einfach gar nicht zu behandeln.
Die richtige Korrektur: Den übrig gebliebenen Wert dem Typ never zuweisen.
never beschreibt einen Wert, der logischerweise nicht auftreten kann. Sobald Ihre switch-Anweisung alle Elemente der Union abgedeckt hat, wird alles, was im default-Fall übrig bleibt, auf never eingegrenzt. Wenn später jemand die Union um eine neue Variante erweitert, weist der Compiler sofort einen Fehler in dieser Zeile aus, weil der verbleibende Wert nicht mehr zu never passt:
type NotificationType = "email" | "sms" | "push";
function sendNotification(type: NotificationType) {
switch (type) {
case "email":
// Handle email
break;
case "sms":
// Handle SMS
break;
case "push":
// Handle push notification
break;
default: {
// If someone adds "in_app" to NotificationType and forgets to update this function,
// TypeScript will raise a compile-time error right here.
const _exhaustiveCheck: never = type;
return _exhaustiveCheck;
}
}
}
Schnellreferenz: Ersatz für any
Wie man dies in Ihrem Team durchsetzt
Das Verständnis dieser Ersatzlösungen behebt nur die Hälfte des Problems – die andere Hälfte besteht darin, zu verhindern, dass any wieder eingeschleust wird, sobald ein Termin näher rückt.
Fügen Sie die Beschränkung direkt in die Konfiguration Ihres Projekts ein, damit sie automatisch durchgesetzt wird:
In tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
In Ihrer ESLint-Konfiguration (.eslintrc.json):
{
"rules": {
"@typescript-eslint/no-explicit-any": "error"
}
}
Der ganze Sinn von TypeScript besteht darin, Ihnen Vertrauen zu geben – Vertrauen beim Refactoring, Vertrauen bei der Bereitstellung sowie eine Autocomplete-Funktion des Editors, die Sie vor ständigen Besuchen in der Dokumentation bewahrt. Das Weglassen von any kann anfangs einschränkend wirken. Doch sobald unknown, Generics und diskriminierte Unionen dessen Platz einnehmen, beginnt Ihr Code sich selbst zu erklären, die Laufzeitfehler nehmen stark ab, und der Compiler funktioniert endlich als der Schutzmechanismus, für den er konzipiert wurde.