Wenn Sie komplexe Webanwendungen erstellen, ist TypeScript wahrscheinlich die Programmiersprache Ihrer Wahl. TypeScript ist bekannt für sein starkes Typsystem und seine statischen Analysefunktionen, was es zu einem leistungsstarken Werkzeug macht, um sicherzustellen, dass Ihr Code robust und fehlerfrei ist.
Darüber hinaus beschleunigt es den Entwicklungsprozess durch die Integration mit Code-Editoren, sodass Entwickler effizienter durch den Code navigieren können und genauere Hinweise und automatische Vervollständigung erhalten sowie ein sicheres Refactoring großer Codemengen ermöglichen.
Der Compiler ist das Herzstück von TypeScript und verantwortlich für die Überprüfung der Typkorrektheit und die Umwandlung von TypeScript-Code in JavaScript. Um jedoch die Leistungsfähigkeit von TypeScript voll auszunutzen, ist es wichtig, den Compiler richtig zu konfigurieren.
Jedes TypeScript-Projekt verfügt über eine oder mehrere tsconfig.json
Dateien, die alle Konfigurationsoptionen für den Compiler enthalten.
Die Konfiguration von tsconfig ist ein entscheidender Schritt, um eine optimale Typsicherheit und Entwicklererfahrung in Ihren TypeScript-Projekten zu erreichen. Indem Sie sich die Zeit nehmen, alle wichtigen Faktoren sorgfältig abzuwägen, können Sie den Entwicklungsprozess beschleunigen und sicherstellen, dass Ihr Code robust und fehlerfrei ist.
Die Standardkonfiguration in tsconfig kann dazu führen, dass Entwickler die meisten Vorteile von TypeScript verpassen. Dies liegt daran, dass viele leistungsstarke Funktionen zur Typprüfung nicht möglich sind. Mit „Standard“-Konfiguration meine ich eine Konfiguration, bei der keine Typprüfungs-Compileroptionen festgelegt sind.
Zum Beispiel:
{ "compilerOptions": { "target": "esnext", "module": "esnext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, }, "include": ["src"] }
Das Fehlen mehrerer wichtiger Konfigurationsoptionen kann aus zwei Hauptgründen zu einer geringeren Codequalität führen. Erstens kann der Compiler von TypeScript in verschiedenen Fällen null
und undefined
Typen falsch verarbeiten.
Zweitens kann der Typ any
unkontrolliert in Ihrer Codebasis auftauchen, was dazu führt, dass die Typprüfung für diesen Typ deaktiviert wird.
Glücklicherweise lassen sich diese Probleme leicht beheben, indem Sie einige Optionen in der Konfiguration anpassen.
{ "compilerOptions": { "strict": true } }
Der strikte Modus ist eine wesentliche Konfigurationsoption, die stärkere Garantien für die Programmkorrektheit bietet, indem sie eine breite Palette von Verhaltensweisen bei der Typprüfung ermöglicht.
Die Aktivierung des strikten Modus in der tsconfig-Datei ist ein entscheidender Schritt zur Erreichung maximaler Typsicherheit und einer besseren Entwicklererfahrung.
Die Konfiguration von tsconfig erfordert etwas mehr Aufwand, kann aber die Qualität Ihres Projekts erheblich verbessern.
Die strict
Compileroption aktiviert alle Optionen der Strict-Mode-Familie, zu denen unter anderem noImplicitAny
, strictNullChecks
und strictFunctionTypes
gehören.
Diese Optionen können auch separat konfiguriert werden, es wird jedoch nicht empfohlen, eine davon zu deaktivieren. Schauen wir uns Beispiele an, um zu sehen, warum.
{ "compilerOptions": { "noImplicitAny": true } }
Der Typ any
ist eine gefährliche Lücke im statischen Typsystem und seine Verwendung deaktiviert alle Typprüfungsregeln. Dadurch gehen alle Vorteile von TypeScript verloren: Fehler werden übersehen, Code-Editor-Hinweise funktionieren nicht mehr richtig und so weiter.
Die Verwendung von any
ist nur in extremen Fällen oder für Prototyping-Anforderungen in Ordnung. Trotz unserer besten Bemühungen kann sich der Typ any
manchmal implizit in eine Codebasis einschleichen.
Standardmäßig verzeiht uns der Compiler viele Fehler im Gegenzug dafür, dass any
in einer Codebasis auftauchen. Insbesondere ermöglicht uns TypeScript, den Typ von Variablen nicht anzugeben, selbst wenn der Typ nicht automatisch abgeleitet werden kann.
Das Problem besteht darin, dass wir versehentlich vergessen, den Typ einer Variablen anzugeben, beispielsweise für ein Funktionsargument. Anstatt einen Fehler anzuzeigen, leitet TypeScript automatisch den Typ der Variablen als any
ab.
function parse(str) { // ^? any return str.split(''); } // TypeError: str.split is not a function const res1 = parse(42); const res2 = parse('hello'); // ^? any
Wenn Sie die Compileroption noImplicitAny
aktivieren, markiert der Compiler alle Stellen, an denen der Typ einer Variablen automatisch als any
abgeleitet wird. In unserem Beispiel fordert uns TypeScript auf, den Typ für das Funktionsargument anzugeben.
function parse(str) { // ^ Error: Parameter 'str' implicitly has an 'any' type. return str.split(''); }
Wenn wir den Typ angeben, erkennt TypeScript schnell den Fehler, eine Zahl an einen String-Parameter zu übergeben. Auch der Rückgabewert der Funktion, der in der Variablen res2
gespeichert ist, hat den richtigen Typ.
function parse(str: string) { return str.split(''); } const res1 = parse(42); // ^ Error: Argument of type 'number' is not // assignable to parameter of type 'string' const res2 = parse('hello'); // ^? string[]
{ "compilerOptions": { "useUnknownInCatchVariables": true } }
Die Konfiguration useUnknownInCatchVariables
ermöglicht die sichere Behandlung von Ausnahmen in Try-Catch-Blöcken. Standardmäßig geht TypeScript davon aus, dass der Fehlertyp in einem Catch-Block any
ist, sodass wir mit dem Fehler alles machen können.
Beispielsweise könnten wir den abgefangenen Fehler unverändert an eine Protokollierungsfunktion übergeben, die eine Instanz von Error
akzeptiert.
function logError(err: Error) { // ... } try { return JSON.parse(userInput); } catch (err) { // ^? any logError(err); }
In der Realität gibt es jedoch keine Garantien für die Art des Fehlers, und wir können seinen wahren Typ erst zur Laufzeit bestimmen, wenn der Fehler auftritt. Wenn die Protokollierungsfunktion etwas empfängt, das kein Error
ist, führt dies zu einem Laufzeitfehler.
Daher ändert die Option useUnknownInCatchVariables
“ den Fehlertyp von any
auf „ unknown
“, um uns daran zu erinnern, den Fehlertyp zu überprüfen, bevor wir etwas dagegen unternehmen.
try { return JSON.parse(userInput); } catch (err) { // ^? unknown // Now we need to check the type of the value if (err instanceof Error) { logError(err); } else { logError(new Error('Unknown Error')); } }
Jetzt fordert uns TypeScript auf, den Typ der err
Variablen zu überprüfen, bevor wir sie an die logError
Funktion übergeben, was zu korrekterem und sichererem Code führt. Leider hilft diese Option nicht bei Tippfehlern in promise.catch()
-Funktionen oder Callback-Funktionen.
Aber wir werden im nächsten Artikel any
besprechen, mit solchen Fällen umzugehen.
{ "compilerOptions": { "strictBindCallApply": true } }
Eine weitere Option korrigiert das Erscheinungsbild any
funktionsinternen Aufrufe über call
und apply
. Dies ist ein seltenerer Fall als die ersten beiden, aber es ist dennoch wichtig, ihn zu berücksichtigen. Standardmäßig prüft TypeScript überhaupt keine Typen in solchen Konstruktionen.
Wir können beispielsweise alles als Argument an eine Funktion übergeben und erhalten am Ende immer den Typ any
.
function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? any const n2 = parse.call(undefined, false); // ^? any
Durch die Aktivierung der strictBindCallApply
Option wird TypeScript intelligenter, sodass der Rückgabetyp korrekt als number
abgeleitet wird. Und wenn versucht wird, ein Argument des falschen Typs zu übergeben, zeigt TypeScript auf den Fehler.
function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? number const n2 = parse.call(undefined, false); // ^ Argument of type 'boolean' is not // assignable to parameter of type 'string'.
{ "compilerOptions": { "noImplicitThis": true } }
Die nächste Option, die dabei helfen kann, das Auftreten von any
Fehlern in Ihrem Projekt zu verhindern, korrigiert die Handhabung des Ausführungskontexts in Funktionsaufrufen. Aufgrund der dynamischen Natur von JavaScript ist es schwierig, den Kontexttyp innerhalb einer Funktion statisch zu bestimmen.
Standardmäßig verwendet TypeScript in solchen Fällen den Typ any
für den Kontext und gibt keine Warnungen aus.
class Person { private name: string; constructor(name: string) { this.name = name; } getName() { return function () { return this.name; // ^ 'this' implicitly has type 'any' because // it does not have a type annotation. }; } }
Durch Aktivieren der Compileroption noImplicitThis
werden wir aufgefordert, den Kontexttyp für eine Funktion explizit anzugeben. Auf diese Weise können wir im obigen Beispiel den Fehler abfangen, der beim Zugriff auf den Funktionskontext statt auf das name
der Person
Klasse entsteht.
{ "compilerOptions": { "strictNullChecks": true } }
Als nächstes führen mehrere Optionen, die im strict
Modus enthalten sind, nicht dazu, dass der Typ „ any
in der Codebasis erscheint. Allerdings verschärfen sie das Verhalten des TS-Compilers und ermöglichen das Auffinden von Fehlern während der Entwicklung.
Die erste dieser Optionen korrigiert die Behandlung von null
und undefined
in TypeScript. Standardmäßig geht TypeScript davon aus, dass null
und undefined
gültige Werte für jeden Typ sind, was zu unerwarteten Laufzeitfehlern führen kann.
Durch die Aktivierung der Compileroption strictNullChecks
wird der Entwickler gezwungen, explizit Fälle zu behandeln, in denen null
und undefined
auftreten können.
Betrachten Sie beispielsweise den folgenden Code:
const users = [ { name: 'Oby', age: 12 }, { name: 'Heera', age: 32 }, ]; const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } console.log(loggedInUser.age); // ^ TypeError: Cannot read properties of undefined
Dieser Code wird fehlerfrei kompiliert, kann jedoch einen Laufzeitfehler auslösen, wenn der Benutzer mit dem Namen „Max“ nicht im System vorhanden ist und users.find()
undefined
zurückgibt. Um dies zu verhindern, können wir die Compileroption strictNullChecks
aktivieren.
Jetzt zwingt uns TypeScript dazu, explizit mit der Möglichkeit umzugehen, dass null
oder undefined
von users.find()
zurückgegeben werden.
const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } | undefined if (loggedInUser) { console.log(loggedInUser.age); }
Durch den expliziten Umgang mit der Möglichkeit von null
und undefiined
können wir Laufzeitfehler vermeiden und sicherstellen, dass unser Code robuster und fehlerfrei ist.
{ "compilerOptions": { "strictFunctionTypes": true } }
Durch die Aktivierung strictFunctionTypes
wird der Compiler von TypeScript intelligenter. Vor Version 2.6 hat TypeScript die Kontravarianz von Funktionsargumenten nicht überprüft. Dies führt zu Laufzeitfehlern, wenn die Funktion mit einem Argument des falschen Typs aufgerufen wird.
Selbst wenn ein Funktionstyp beispielsweise sowohl Zeichenfolgen als auch Zahlen verarbeiten kann, können wir diesem Typ eine Funktion zuweisen, die nur Zeichenfolgen verarbeiten kann. Wir können dieser Funktion immer noch eine Zahl übergeben, erhalten jedoch einen Laufzeitfehler.
function greet(x: string) { console.log("Hello, " + x.toLowerCase()); } type StringOrNumberFn = (y: string | number) => void; // Incorrect Assignment const func: StringOrNumberFn = greet; // TypeError: x.toLowerCase is not a function func(10);
Glücklicherweise behebt die Aktivierung der Option strictFunctionTypes
dieses Verhalten, und der Compiler kann diese Fehler zur Kompilierungszeit abfangen und uns eine detaillierte Meldung über die Typinkompatibilität in Funktionen anzeigen.
const func: StringOrNumberFn = greet; // ^ Type '(x: string) => void' is not assignable to type 'StringOrNumberFn'. // Types of parameters 'x' and 'y' are incompatible. // Type 'string | number' is not assignable to type 'string'. // Type 'number' is not assignable to type 'string'.
{ "compilerOptions": { "strictPropertyInitialization": true } }
Zu guter Letzt ermöglicht die Option strictPropertyInitialization
die Überprüfung der obligatorischen Klasseneigenschaftsinitialisierung für Typen, die undefined
nicht als Wert enthalten.
Im folgenden Code hat der Entwickler beispielsweise vergessen, die email
Eigenschaft zu initialisieren. Standardmäßig würde TypeScript diesen Fehler nicht erkennen und es könnte zur Laufzeit ein Problem auftreten.
class UserAccount { name: string; email: string; constructor(name: string) { this.name = name; // Forgot to assign a value to this.email } }
Wenn jedoch die Option strictPropertyInitialization
aktiviert ist, wird TypeScript dieses Problem für uns hervorheben.
email: string; // ^ Error: Property 'email' has no initializer and // is not definitely assigned in the constructor.
{ "compilerOptions": { "noUncheckedIndexedAccess": true } }
Die Option noUncheckedIndexedAccess
ist nicht Teil des strict
Modus, aber eine weitere Option, die zur Verbesserung der Codequalität in Ihrem Projekt beitragen kann. Es ermöglicht die Überprüfung von Indexzugriffsausdrücken auf einen null
oder undefined
Rückgabetyp, wodurch Laufzeitfehler verhindert werden können.
Betrachten Sie das folgende Beispiel, in dem wir ein Objekt zum Speichern zwischengespeicherter Werte haben. Wir erhalten dann den Wert für einen der Schlüssel. Natürlich können wir nicht garantieren, dass der Wert für den gewünschten Schlüssel tatsächlich im Cache vorhanden ist.
Standardmäßig geht TypeScript davon aus, dass der Wert existiert und den Typ string
hat. Dies kann zu einem Laufzeitfehler führen.
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string console.log(value.toUpperCase()); // ^ TypeError: Cannot read properties of undefined
Das Aktivieren der Option noUncheckedIndexedAccess
in TypeScript erfordert die Überprüfung von Indexzugriffsausdrücken auf undefined
Rückgabetypen, was uns helfen kann, Laufzeitfehler zu vermeiden. Dies gilt auch für den Zugriff auf Elemente in einem Array.
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string | undefined if (value) { console.log(value.toUpperCase()); }
Basierend auf den besprochenen Optionen wird dringend empfohlen, die Optionen strict
und noUncheckedIndexedAccess
in tsconfig.json
Datei Ihres Projekts zu aktivieren, um optimale Typsicherheit zu gewährleisten.
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, } }
Wenn Sie die Option strict
bereits aktiviert haben, können Sie erwägen, die folgenden Optionen zu entfernen, um eine Duplizierung der Option strict: true
zu vermeiden:
noImplicitAny
useUnknownInCatchVariables
strictBindCallApply
noImplicitThis
strictFunctionTypes
strictNullChecks
strictPropertyInitialization
Es wird außerdem empfohlen, die folgenden Optionen zu entfernen, die das Typsystem schwächen oder Laufzeitfehler verursachen können:
keyofStringsOnly
noStrictGenericChecks
suppressImplicitAnyIndexErrors
suppressExcessPropertyErrors
Durch sorgfältiges Abwägen und Konfigurieren dieser Optionen können Sie optimale Typsicherheit und ein besseres Entwicklererlebnis in Ihren TypeScript-Projekten erreichen.
TypeScript hat in seiner Entwicklung einen langen Weg zurückgelegt und seinen Compiler und sein Typsystem ständig verbessert. Um die Abwärtskompatibilität zu gewährleisten, ist die TypeScript-Konfiguration jedoch komplexer geworden und bietet viele Optionen, die die Qualität der Typprüfung erheblich beeinträchtigen können.
Durch sorgfältiges Abwägen und Konfigurieren dieser Optionen können Sie optimale Typsicherheit und ein besseres Entwicklererlebnis in Ihren TypeScript-Projekten erreichen. Es ist wichtig zu wissen, welche Optionen in einer Projektkonfiguration aktiviert und entfernt werden müssen.
Wenn Sie die Folgen der Deaktivierung bestimmter Optionen verstehen, können Sie für jede einzelne Option fundierte Entscheidungen treffen.
Es ist wichtig zu bedenken, dass eine strikte Eingabe Konsequenzen haben kann. Um effektiv mit der dynamischen Natur von JavaScript umgehen zu können, müssen Sie über gute Kenntnisse von TypeScript verfügen, die über die einfache Angabe von „Zahl“ oder „Zeichenfolge“ nach einer Variablen hinausgehen.
Sie müssen mit komplexeren Konstrukten und dem TypeScript-First-Ökosystem von Bibliotheken und Tools vertraut sein, um auftretende typbezogene Probleme effektiver lösen zu können.
Daher ist das Schreiben von Code möglicherweise etwas aufwändiger, aber meiner Erfahrung nach lohnt sich dieser Aufwand für langfristige Projekte.
Ich hoffe, Sie haben aus diesem Artikel etwas Neues gelernt. Dies ist der erste Teil einer Serie. Im nächsten Artikel besprechen wir, wie wir durch die Verbesserung der Typen in der Standardbibliothek von TypeScript eine bessere Typsicherheit und Codequalität erreichen können. Bleiben Sie dran und vielen Dank fürs Lesen!