paint-brush
TypeScript wirklich „stark typisiert“ machenvon@nodge
17,366 Lesungen
17,366 Lesungen

TypeScript wirklich „stark typisiert“ machen

von Maksim Zemskov12m2023/09/10
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

TypeScript bietet den Typ „Any“ für Situationen, in denen die Form der Daten nicht im Voraus bekannt ist. Eine übermäßige Verwendung dieses Typs kann jedoch zu Problemen mit der Typsicherheit, der Codequalität und der Entwicklererfahrung führen. In diesem Artikel werden die mit dem Typ „Any“ verbundenen Risiken untersucht, potenzielle Quellen für seine Aufnahme in eine Codebasis identifiziert und Strategien zur Steuerung seiner Verwendung im gesamten Projekt bereitgestellt.

People Mentioned

Mention Thumbnail
featured image - TypeScript wirklich „stark typisiert“ machen
Maksim Zemskov HackerNoon profile picture
0-item
1-item

TypeScript behauptet, eine stark typisierte Programmiersprache zu sein, die auf JavaScript aufbaut und bessere Tools in jeder Größenordnung bietet. Allerdings enthält TypeScript den Typ any , der sich oft implizit in eine Codebasis einschleichen kann und zum Verlust vieler Vorteile von TypeScript führt.


In diesem Artikel werden Möglichkeiten untersucht, die Kontrolle über any Typen in TypeScript-Projekten zu übernehmen. Machen Sie sich bereit, die Leistungsfähigkeit von TypeScript zu nutzen, um ultimative Typsicherheit zu erreichen und die Codequalität zu verbessern.

Nachteile der Verwendung von Any in TypeScript

TypeScript bietet eine Reihe zusätzlicher Tools zur Verbesserung der Entwicklererfahrung und Produktivität:


  • Es hilft, Fehler frühzeitig in der Entwicklungsphase zu erkennen.
  • Es bietet eine hervorragende automatische Vervollständigung für Code-Editoren und IDEs.
  • Es ermöglicht ein einfaches Refactoring großer Codebasen durch fantastische Code-Navigationstools und automatisches Refactoring.
  • Es vereinfacht das Verständnis einer Codebasis, indem es zusätzliche Semantik und explizite Datenstrukturen durch Typen bereitstellt.


Sobald Sie jedoch beginnen, den Typ any in Ihrer Codebasis zu verwenden, verlieren Sie alle oben aufgeführten Vorteile. Der Typ any ist eine gefährliche Lücke im Typsystem und seine Verwendung deaktiviert alle Typprüfungsfunktionen sowie alle Tools, die von der Typprüfung abhängen. Dadurch gehen alle Vorteile von TypeScript verloren: Fehler werden übersehen, Code-Editoren werden weniger nützlich und vieles mehr.


Betrachten Sie zum Beispiel das folgende Beispiel:


 function parse(data: any) { return data.split(''); } // Case 1 const res1 = parse(42); // ^ TypeError: data.split is not a function // Case 2 const res2 = parse('hello'); // ^ any


Im Code oben:


  • Sie werden die automatische Vervollständigung innerhalb der parse Funktion vermissen. Wenn Sie data. In Ihrem Editor erhalten Sie keine korrekten Vorschläge für die verfügbaren Methoden für data .
  • Im ersten Fall liegt ein TypeError: data.split is not a function da wir eine Zahl anstelle einer Zeichenfolge übergeben haben. TypeScript kann den Fehler nicht hervorheben, da any die Typprüfung deaktiviert wird.
  • Im zweiten Fall hat die Variable res2 ebenfalls den Typ any . Dies bedeutet, dass eine einzelne Verwendung eines any Codes einen kaskadierenden Effekt auf einen großen Teil einer Codebasis haben kann.


Die Verwendung von any ist nur in extremen Fällen oder für Prototyping-Anforderungen in Ordnung. Im Allgemeinen ist es besser, die Verwendung any zu vermeiden, um das Beste aus TypeScript herauszuholen.

Woher der Typ „Any“ kommt

Es ist wichtig, die Quellen des Typs „ any in einer Codebasis zu kennen, da das explizite Schreiben von „ any nicht die einzige Option ist. Obwohl wir uns nach besten Kräften bemühen, die Verwendung des Typs any zu vermeiden, kann er sich manchmal implizit in eine Codebasis einschleichen.


Es gibt vier Hauptquellen any Art in einer Codebasis:

  1. Compiler-Optionen in tsconfig.
  2. Die Standardbibliothek von TypeScript.
  3. Projektabhängigkeiten.
  4. Explizite Verwendung von any in einer Codebasis.


Zu den ersten beiden Punkten habe ich bereits Artikel zu den wichtigsten Überlegungen in tsconfig und zur Verbesserung der Standardbibliothekstypen geschrieben. Bitte schauen Sie sich diese an, wenn Sie die Typensicherheit in Ihren Projekten verbessern möchten.


Dieses Mal konzentrieren wir uns auf automatische Tools zur Steuerung des Erscheinungsbilds eines any Typs in einer Codebasis.

Stufe 1: Verwendung von ESLint

ESLint ist ein beliebtes statisches Analysetool, das von Webentwicklern verwendet wird, um Best Practices und Codeformatierung sicherzustellen. Es kann verwendet werden, um Codierungsstile durchzusetzen und Code zu finden, der bestimmten Richtlinien nicht entspricht.


Dank des Plugins typesctipt-eslint kann ESLint auch mit TypeScript-Projekten verwendet werden. Höchstwahrscheinlich wurde dieses Plugin bereits in Ihrem Projekt installiert. Wenn nicht, können Sie dem offiziellen Leitfaden für die ersten Schritte folgen.


Die häufigste Konfiguration für typescript-eslint ist wie folgt:


 module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, };


Diese Konfiguration ermöglicht eslint , TypeScript auf Syntaxebene zu verstehen, sodass Sie einfache eslint-Regeln schreiben können, die für manuell geschriebene Typen in einem Code gelten. Sie können beispielsweise die explizite Verwendung von any verbieten.


Die recommended Voreinstellung enthält einen sorgfältig ausgewählten Satz von ESLint-Regeln zur Verbesserung der Codekorrektheit. Obwohl empfohlen wird, die gesamte Voreinstellung zu verwenden, konzentrieren wir uns in diesem Artikel nur auf die no-explicit-any Regel.

nein-explizit-irgendein

Der strikte Modus von TypeScript verhindert die Verwendung von impliziten „ any , verhindert jedoch nicht die explizite Verwendung von „ any . Die no-explicit-any Regel hilft dabei, das manuelle Schreiben any irgendwo in einer Codebasis zu verhindern.


 // ❌ Incorrect function loadPokemons(): any {} // ✅ Correct function loadPokemons(): unknown {} // ❌ Incorrect function parsePokemons(data: Response<any>): Array<Pokemon> {} // ✅ Correct function parsePokemons(data: Response<unknown>): Array<Pokemon> {} // ❌ Incorrect function reverse<T extends Array<any>>(array: T): T {} // ✅ Correct function reverse<T extends Array<unknown>>(array: T): T {}


Der Hauptzweck dieser Regel besteht darin, den Einsatz von any im gesamten Team zu verhindern. Dies ist ein Mittel, um die Zustimmung des Teams zu stärken, dass von der Verwendung any im Projekt abgeraten wird.


Dies ist ein entscheidendes Ziel, da bereits die einmalige Verwendung von any aufgrund der Typinferenz kaskadierende Auswirkungen auf einen erheblichen Teil der Codebasis haben kann. Von der endgültigen Typensicherheit ist dies jedoch noch weit entfernt.

Warum „no-explicit-any“ nicht genug ist

Obwohl wir uns mit der expliziten Verwendung any befasst haben, gibt es immer noch viele implizite „ any innerhalb der Abhängigkeiten eines Projekts, einschließlich npm-Paketen und der Standardbibliothek von TypeScript.


Betrachten Sie den folgenden Code, der wahrscheinlich in jedem Projekt vorkommt:


 const response = await fetch('https://pokeapi.co/api/v2/pokemon'); const pokemons = await response.json(); // ^? any const settings = JSON.parse(localStorage.getItem('user-settings')); // ^? any


Sowohl den Variablen pokemons als auch settings wurde implizit der Typ „ any zugewiesen. Weder no-explicit-any noch der strikte Modus von TypeScript werden uns in diesem Fall warnen. Noch nicht.


Dies liegt daran, dass die Typen für response.json() und JSON.parse() aus der Standardbibliothek von TypeScript stammen, in der diese Methoden eine explizite „ any -Annotation haben. Wir können immer noch manuell einen besseren Typ für unsere Variablen angeben, aber in der Standardbibliothek gibt es fast 1.200 any davon. Es ist fast unmöglich, sich an alle Fälle zu erinnern, in denen sich any aus der Standardbibliothek in unsere Codebasis einschleichen konnte.


Das Gleiche gilt für externe Abhängigkeiten. Es gibt viele schlecht typisierte Bibliotheken in npm, wobei die meisten noch in JavaScript geschrieben sind. Daher kann die Verwendung solcher Bibliotheken leicht zu vielen impliziten any in einer Codebasis führen.


Im Allgemeinen gibt es immer noch viele Möglichkeiten, any in unseren Code einzuschleichen.

Stufe 2: Verbesserung der Fähigkeiten zur Typprüfung

Idealerweise hätten wir gerne eine Einstellung in TypeScript, die den Compiler dazu bringt, sich über jede Variable zu beschweren, die aus irgendeinem Grund den Typ „ any “ erhalten hat. Leider existiert eine solche Einstellung derzeit nicht und wird voraussichtlich auch nicht hinzugefügt.


Wir können dieses Verhalten erreichen, indem wir den typgeprüften Modus des typescript-eslint -Plugins verwenden. Dieser Modus arbeitet mit TypeScript zusammen, um den ESLint-Regeln vollständige Typinformationen vom TypeScript-Compiler bereitzustellen. Mit diesen Informationen ist es möglich, komplexere ESLint-Regeln zu schreiben, die die Typprüfungsfunktionen von TypeScript wesentlich erweitern. Beispielsweise kann eine Regel alle Variablen mit dem Typ „ any finden, unabhängig davon, wie „ any ermittelt wurde.


Um typbewusste Regeln zu verwenden, müssen Sie die ESLint-Konfiguration leicht anpassen:


 module.exports = { extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, root: true, };


Um die Typinferenz für typescript-eslint zu aktivieren, fügen Sie parserOptions zur ESLint-Konfiguration hinzu. Ersetzen Sie dann die recommended Voreinstellung durch recommended-type-checked . Die letztere Voreinstellung fügt etwa 17 neue leistungsstarke Regeln hinzu. In diesem Artikel konzentrieren wir uns nur auf fünf davon.

Kein-unsicheres-Argument

Die no-unsafe-argument Regel sucht nach Funktionsaufrufen, bei denen eine Variable vom Typ „ any als Parameter übergeben wird. In diesem Fall geht die Typprüfung verloren und alle Vorteile der starken Typisierung gehen ebenfalls verloren.


Betrachten wir beispielsweise eine saveForm Funktion, die ein Objekt als Parameter erfordert. Angenommen, wir empfangen JSON, analysieren es und erhalten einen any Typ.


 // ❌ Incorrect function saveForm(values: FormValues) { console.log(values); } const formValues = JSON.parse(userInput); // ^? any saveForm(formValues); // ^ Unsafe argument of type `any` assigned // to a parameter of type `FormValues`.


Wenn wir die Funktion saveForm mit diesem Parameter aufrufen, kennzeichnet die Regel no-unsafe-argument sie als unsicher und erfordert, dass wir den entsprechenden Typ für die value angeben.


Diese Regel ist leistungsstark genug, um verschachtelte Datenstrukturen innerhalb von Funktionsargumenten gründlich zu untersuchen. Daher können Sie sicher sein, dass die Übergabe von Objekten als Funktionsargumente niemals untypisierte Daten enthalten wird.


 // ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. });


Der beste Weg, den Fehler zu beheben, besteht darin, die Typeingrenzung von TypeScript oder eine Validierungsbibliothek wie Zod oder Superstruct zu verwenden. Schreiben wir beispielsweise die Funktion parseFormValues , die den genauen Typ der analysierten Daten einschränkt.


 // ✅ Correct function parseFormValues(data: unknown): FormValues { if ( typeof data === 'object' && data !== null && 'name' in data && typeof data['name'] === 'string' && 'address' in data && typeof data.address === 'string' ) { const { name, address } = data; return { name, address }; } throw new Error('Failed to parse form values'); } const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues saveForm(formValues);


Beachten Sie, dass es zulässig ist, den Typ any als Argument an eine Funktion zu übergeben, die „ unknown akzeptiert, da damit keine Sicherheitsbedenken verbunden sind.


Das Schreiben von Datenvalidierungsfunktionen kann eine mühsame Aufgabe sein, insbesondere wenn es um große Datenmengen geht. Daher lohnt es sich, über den Einsatz einer Datenvalidierungsbibliothek nachzudenken. Mit Zod würde der Code beispielsweise so aussehen:


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); const formValues = schema.parse(JSON.parse(userInput)); // ^? { name: string, address: string } saveForm(formValues);


Keine-unsichere-Zuweisung

Die no-unsafe-assignment Regel sucht nach Variablenzuweisungen, in denen ein Wert den Typ any hat. Solche Zuweisungen können den Compiler zu der Annahme verleiten, dass eine Variable einen bestimmten Typ hat, während die Daten tatsächlich einen anderen Typ haben könnten.


Betrachten Sie das vorherige Beispiel der JSON-Analyse:


 // ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value


Dank der no-unsafe-assignment Regel können wir any Typ abfangen, noch bevor wir formValues an anderer Stelle übergeben. Die Fixierungsstrategie bleibt dieselbe: Wir können die Typeingrenzung verwenden, um dem Wert der Variablen einen bestimmten Typ zuzuweisen.


 // ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues


No-unsafe-member-access und No-unsafe-call

Diese beiden Regeln werden viel seltener ausgelöst. Meiner Erfahrung nach sind sie jedoch sehr hilfreich, wenn Sie versuchen, schlecht typisierte Abhängigkeiten von Drittanbietern zu verwenden.


Die Regel no-unsafe-member-access verhindert, dass wir auf Objekteigenschaften zugreifen, wenn eine Variable den Typ „ any hat, da sie null oder undefined sein kann.


Die no-unsafe-call Regel verhindert, dass wir eine Variable mit dem Typ „ any als Funktion aufrufen, da es sich möglicherweise nicht um eine Funktion handelt.


Stellen wir uns vor, wir hätten eine schlecht typisierte Drittanbieter-Bibliothek namens untyped-auth :


 // ❌ Incorrect import { authenticate } from 'untyped-auth'; // ^? any const userInfo = authenticate(); // ^? any ^ Unsafe call of an `any` typed value. console.log(userInfo.name); // ^ Unsafe member access .name on an `any` value.


Der Linter hebt zwei Probleme hervor:

  • Der Aufruf der authenticate Funktion kann unsicher sein, da wir möglicherweise vergessen, wichtige Argumente an die Funktion zu übergeben.
  • Das Lesen der name aus dem userInfo Objekt ist unsicher, da sie null ist, wenn die Authentifizierung fehlschlägt.


Der beste Weg, diese Fehler zu beheben, besteht darin, die Verwendung einer Bibliothek mit einer stark typisierten API in Betracht zu ziehen. Wenn dies jedoch keine Option ist, können Siedie Bibliothekstypen selbst erweitern . Ein Beispiel mit den festen Bibliothekstypen würde so aussehen:


 // ✅ Correct import { authenticate } from 'untyped-auth'; // ^? (login: string, password: string) => Promise<UserInfo | null> const userInfo = await authenticate('test', 'pwd'); // ^? UserInfo | null if (userInfo) { console.log(userInfo.name); }


keine unsichere Rückkehr

Die no-unsafe-return Regel hilft dabei, nicht versehentlich den Typ „ any von einer Funktion zurückzugeben, die etwas Spezifischeres zurückgeben sollte. Solche Fälle können den Compiler zu der Annahme verleiten, dass ein zurückgegebener Wert einen bestimmten Typ hat, während die Daten tatsächlich einen anderen Typ haben könnten.


Angenommen, wir haben eine Funktion, die JSON analysiert und ein Objekt mit zwei Eigenschaften zurückgibt.


 // ❌ Incorrect interface FormValues { name: string; address: string; } function parseForm(json: string): FormValues { return JSON.parse(json); // ^ Unsafe return of an `any` typed value. } const form = parseForm('null'); console.log(form.name); // ^ TypeError: Cannot read properties of null


Die parseForm Funktion kann in jedem Teil des Programms, in dem sie verwendet wird, zu Laufzeitfehlern führen, da der geparste Wert nicht überprüft wird. Die no-unsafe-return Regel verhindert solche Laufzeitprobleme.


Dies lässt sich leicht beheben, indem Sie eine Validierung hinzufügen, um sicherzustellen, dass der analysierte JSON-Code mit dem erwarteten Typ übereinstimmt. Lassen Sie uns dieses Mal die Zod-Bibliothek verwenden:


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); function parseForm(json: string): FormValues { return schema.parse(JSON.parse(json)); }


Ein Hinweis zur Leistung

Die Verwendung typgeprüfter Regeln bringt für ESLint einen Leistungseinbußen mit sich, da der Compiler von TypeScript aufgerufen werden muss, um alle Typen abzuleiten. Diese Verlangsamung macht sich hauptsächlich beim Ausführen des Linters in Pre-Commit-Hooks und in CI bemerkbar, beim Arbeiten in einer IDE ist sie jedoch nicht spürbar. Die Typprüfung wird einmal beim IDE-Start durchgeführt und aktualisiert dann die Typen, wenn Sie den Code ändern.


Es ist erwähnenswert, dass allein das Ableiten der Typen schneller funktioniert als der übliche Aufruf des tsc Compilers. Bei unserem neuesten Projekt mit etwa 1,5 Millionen Zeilen TypeScript-Code dauert die Typprüfung durch tsc beispielsweise etwa 11 Minuten, während die zusätzliche Zeit, die für das Bootstrapping der typbewussten Regeln von ESLint erforderlich ist, nur etwa 2 Minuten beträgt.


Für unser Team ist die zusätzliche Sicherheit, die die Verwendung typbewusster statischer Analyseregeln bietet, den Kompromiss wert. Bei kleineren Projekten ist diese Entscheidung noch einfacher zu treffen.

Abschluss

Die Kontrolle der Verwendung von any in TypeScript-Projekten ist entscheidend für die Erzielung optimaler Typsicherheit und Codequalität. Durch die Verwendung des typescript-eslint Plugins können Entwickler alle Vorkommen any Art in ihrer Codebasis identifizieren und beseitigen, was zu einer robusteren und wartbareren Codebasis führt.


Durch die Verwendung typbewusster Eslint-Regeln ist jedes Auftreten des Schlüsselworts „ any in unserer Codebasis eine bewusste Entscheidung und kein Fehler oder Versehen. Dieser Ansatz schützt uns vor der Verwendung in unserem any Code sowie in der Standardbibliothek und in Abhängigkeiten von Drittanbietern.


Insgesamt ermöglicht uns ein typbewusster Linter, ein Maß an Typsicherheit zu erreichen, das dem von statisch typisierten Programmiersprachen wie Java, Go, Rust und anderen ähnelt. Dies vereinfacht die Entwicklung und Wartung großer Projekte erheblich.


Ich hoffe, Sie haben aus diesem Artikel etwas Neues gelernt. Vielen Dank fürs Lesen!

Nützliche Links