Menu

TypeScript Best Practices: 8 Regeln mit Vorher und Nachher

Acht TypeScript-Gewohnheiten, die echte Bugs verhindern: strict anlassen, unknown statt any, Inferenz arbeiten lassen, Unions statt Enums, Konfiguration mit satisfies prüfen, Zustand mit Discriminated Unions modellieren, ! und as vermeiden und Daten readonly machen. Jede mit einem ausführbaren Vorher und Nachher.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Die folgenden Regeln verhindern in echtem TypeScript-Code die meisten Bugs. Jede zeigt zuerst die übliche Variante und dann die bessere, in Code, den du ausführen kannst. Die erste Regel ist die wichtigste: Hör auf, any zu verwenden.

unknown statt any verwenden

any schaltet die Typprüfung für einen Wert und alles, was daraus berechnet wird, ab. unknown akzeptiert ebenfalls jeden Wert, aber du musst ihn vor der Verwendung prüfen, und damit liegt die Prüfung dort, wo die Daten in dein Programm kommen:

Die Grenzen sind die Stellen, an denen Typen nicht mehr garantiert sind: JSON.parse, Antworten von fetch, localStorage, Formulareingaben, Umgebungsvariablen und Nachrichten anderer Prozesse. Validiere dort mit einem Type Guard oder einer Schema-Bibliothek, dann kann der restliche Code seinen Typen vertrauen.

strict anlassen

strict ist in TypeScript 7 der Standard; schalte es nicht ab. Ergänze die Prüfungen, die es auslässt und die die meisten Bugs finden:

{
    "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitOverride": true,
        "noFallthroughCasesInSwitch": true
    }
}

noUncheckedIndexedAccess lässt arr[i] und record[key] undefined enthalten, was sie bei einem fehlenden Index auch zurückgeben. noImplicitOverride verlangt, dass eine Methode einer Unterklasse override angibt, und noFallthroughCasesInSwitch lehnt einen case ab, der in den nächsten läuft.

Inferenz arbeiten lassen

Annotiere, was TypeScript nicht wissen kann: Funktionsparameter und die Rückgabetypen von Funktionen, die andere Module nutzen. Lokale Variablen und Callback-Parameter überlässt du der Inferenz. Eine unnötige Annotation ist nicht nur Rauschen; sie kann einen Typ weiter machen als den Wert:

Ohne den Kommentar @ts-expect-error ist setStatus(annotated) der Fehler TS2345. Die abgeleitete const behält den Literaltyp "active" und wird deshalb akzeptiert. Fahre im Editor mit der Maus über eine Variable, um zu sehen, was abgeleitet wurde, bevor du einen Typ hinzufügst.

Union-Typen statt Enums bevorzugen

Eine Union aus String-Literalen bietet Autovervollständigung und Vollständigkeitsprüfungen ohne erzeugten Code. Brauchst du die Liste der Werte auch zur Laufzeit, leite den Typ aus einem as const Array ab:

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

function canEdit(role: Role): boolean {
  return role !== "viewer";
}

console.log(canEdit("editor"));     // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]

canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.

Ein enum Role { Admin, Viewer } wird zu einem Objekt mit Reverse Mapping kompiliert, und ein numerischer Enum-Parameter akzeptiert jede number-Variable, sogar eine mit einem Wert, den das Enum nicht aufführt. Enums laufen außerdem nicht mit dem Type Stripping von Node (TypeScript enum is not supported in strip-only mode). Die Abwägungen werden auf der Seite zu Enums verglichen.

Konfigurationsobjekte mit satisfies prüfen

Annotierst du ein Objekt mit einem weiten Typ wie Record<string, Route>, werden seine Werte geprüft, aber seine Schlüssel gehen verloren. satisfies prüft dasselbe und behält den genauen Typ:

Nimm eine Annotation, wenn die Variable genau den deklarierten Typ haben soll (ein Funktionsparameter, ein Wert, den du neu zuweist). Nimm satisfies für Lookup-Tabellen, Routen-Maps, Theme-Tokens und andere konstante Objekte.

Zustand mit Discriminated Unions modellieren

Ein einzelnes Objekt mit optionalen Feldern erlaubt Zustände, die nicht vorkommen können: loading: true zusammen mit einem error oder fehlendes data nach einem Erfolg. Eine Union aus Objekten mit einem gemeinsamen Tag erlaubt nur die echten Zustände, und jeder Zweig sieht nur seine eigenen Felder:

Die Zeile mit never ist die Vollständigkeitsprüfung. Fügt jemand einen Zustand hinzu und vergisst, ihn zu behandeln, bricht der Build an dieser Zeile:

Der Fehler lautet index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. Er nennt den unbehandelten Fall. Ergänze case "cancelled":, und es kompiliert.

! und as vermeiden

Die Non-Null-Assertion x! und die Typ-Assertion x as T sagen dem Compiler, er solle aufhören zu prüfen. Keine von beiden ändert den Wert zur Laufzeit, eine falsche Assertion wird also später zu einem Absturz, weit weg von ihrer Ursache:

Ersetze ! durch ?., ??, ein frühes return oder einen geworfenen Fehler mit einer hilfreichen Meldung. Ersetze as durch einen Type Guard, der den Wert tatsächlich prüft. Die eine Assertion, die immer sicher ist, ist as const, weil sie einen Typ nur enger und schreibgeschützt macht. Ein gutes Lint-Setup (no-non-null-assertion und no-explicit-any von typescript-eslint) markiert den Rest.

Daten readonly machen

Markiere Eigenschaften und Arrays als readonly, wenn Code sie nicht ändern soll. Der Compiler lehnt dann push, sort und Zuweisungen ab, und Funktionen geben neue Werte zurück, statt ihre Eingaben zu verändern:

readonly wird nur beim Kompilieren geprüft und nur eine Ebene tief: Das Objekt wird zur Laufzeit nicht eingefroren. Das reicht trotzdem, um die versehentliche Veränderung gemeinsam genutzter Zustände zu finden, die die meisten dieser Bugs verursacht.

Häufig gestellte Fragen

Sollte ich in TypeScript any verwenden?

Im Anwendungscode fast nie. any schaltet die Prüfung für den Wert und alles, was daraus abgeleitet wird, ab. Nimm unknown für Werte, deren Typ du noch nicht kennst, und enge sie mit Prüfungen ein; any bleibt für seltene Notausgänge, mit einem Kommentar, der den Grund erklärt.

Sollte ich in TypeScript jede Variable annotieren?

Nein. Lass TypeScript lokale Variablen und Callback-Parameter ableiten. Annotiere Funktionsparameter (sie lassen sich nicht ableiten) und die Rückgabetypen exportierter Funktionen, damit eine Änderung in der Funktion ihren öffentlichen Typ nicht unbemerkt ändern kann.

Sind Enums in TypeScript schlechte Praxis?

Nicht falsch, aber viele Teams meiden sie. Ein Enum erzeugt Laufzeitcode, läuft nicht mit dem Type Stripping von Node, und ein numerischer Enum-Parameter akzeptiert jede number-Variable, egal welchen Wert sie enthält. Eine Union aus String-Literalen oder ein as const Array mit abgeleitetem Typ bietet dieselbe Autovervollständigung und dieselben Prüfungen ohne erzeugten Code.

Wann sollte ich Typ-Assertions mit as verwenden?

Nur wenn du etwas weißt, was der Compiler nicht wissen kann, und am besten direkt nach einer Prüfung, die es beweist. as ändert zur Laufzeit keinen Wert, also kompiliert {} as User und hat dann kein name. Ein Type Guard, der den Wert prüft, ist in den meisten Fällen das sicherere Werkzeug.

Welche tsconfig-Einstellungen sind für ein neues TypeScript-Projekt am besten?

Lass strict an (der Standard in TypeScript 7) und ergänze noUncheckedIndexedAccess. Viele Projekte aktivieren außerdem noImplicitOverride, noFallthroughCasesInSwitch und verbatimModuleSyntax. Die Konfiguration, die tsc --init schreibt, setzt unter anderem strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes und verbatimModuleSyntax.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S