Kapitel 1. TypeScript kennenlernen

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Bevor wir in die Details eintauchen, hilft dir dieses Kapitel, das große Ganze von TypeScript zu verstehen. Was ist es und wie solltest du darüber denken? Wie verhält es sich zu JavaScript? Sind die Typen nullbar oder nicht? Was hat es mit any auf sich? Und Enten?

TypeScript ist eine ungewöhnliche Sprache, da sie weder in einem Interpreter läuft (wie Python und Ruby) noch zu einer niedrigeren Sprache kompiliert wird (wie Java und C). Stattdessen wird sie in eine andere Hochsprache, JavaScript, kompiliert. Es ist dieses JavaScript, das ausgeführt wird, nicht dein TypeScript. Deshalb ist es wichtig, die Beziehung zwischen TypeScript und JavaScript zu verstehen, damit du ein effektiver TypeScript-Entwickler werden kannst.

Das Typsystem von TypeScript hat auch einige ungewöhnliche Aspekte, die du kennen solltest. In späteren Kapiteln wird das Typsystem noch viel ausführlicher behandelt, aber in diesem Kapitel werden einige der wichtigsten Highlights angesprochen.

Du solltest dieses Kapitel lesen, auch wenn du schon viel TypeScript geschrieben hast. Es wird dir helfen, ein richtiges mentales Modell davon zu entwickeln, was TypeScript ist und wie das Typsystem funktioniert, und es wird vielleicht einige Missverständnisse ausräumen, von denen du gar nicht wusstest, dass du sie hast.

Punkt 1: Verstehe die Beziehung zwischen TypeScript und JavaScript

Wenn du lange mit TypeScript arbeitest, wirst du unweigerlich den Satz "TypeScript ist eine Obermenge von JavaScript" oder "TypeScript ist eine typisierte Obermenge von JavaScript" hören. Aber was bedeutet das genau? Und wie ist die Beziehung zwischen TypeScript und JavaScript? Da diese Sprachen so eng miteinander verbunden sind, ist ein gutes Verständnis ihrer Beziehung zueinander die Grundlage für eine gute Nutzung von TypeScript.

A ist eine "Obermenge" von B, wenn alles, was in B steht, auch in A steht. TypeScript ist ein Superset von JavaScript im syntaktischen Sinne: Solange dein JavaScript-Programm keine Syntaxfehler hat, ist es auch ein TypeScript-Programm. Es ist sehr wahrscheinlich, dass der TypeScript-Typ-Checker einige Probleme in deinem Code aufzeigt. Aber das ist ein unabhängiges Problem. TypeScript wird deinen Code trotzdem analysieren und JavaScript ausgeben. (Dies ist ein weiterer wichtiger Teil der Beziehung. Darauf gehen wir in Punkt 3 näher ein.)

TypeScript-Dateien verwenden eine .ts-Erweiterung und nicht die .js-Erweiterung einer JavaScript-Datei.1 Das bedeutet aber nicht, dass TypeScript eine völlig andere Sprache ist! Da TypeScript eine Obermenge von JavaScript ist, ist der Code in deinen .js-Dateien bereits TypeScript. Das Umbenennen von main.js in main.ts ändert daran nichts.

Das ist enorm hilfreich, wenn du eine bestehende JavaScript-Codebasis zu TypeScript migrierst. Das bedeutet, dass du deinen Code nicht in einer anderen Sprache neu schreiben musst, um TypeScript zu verwenden und die Vorteile zu nutzen, die es bietet. Das wäre nicht der Fall, wenn du deinen JavaScript-Code in einer Sprache wie Java umschreiben würdest. Dieser sanfte Migrationspfad ist eine der besten Eigenschaften von TypeScript. Zu diesem Thema wird es in Kapitel 10 noch viel mehr zu sagen geben.

Alle JavaScript-Programme sind TypeScript-Programme, aber das Gegenteil ist nicht der Fall: Es gibt TypeScript-Programme, die keine JavaScript-Programme sind. Das liegt daran, dass TypeScript eine zusätzliche Syntax für die Angabe von Typen enthält. (Es gibt auch noch einige andere Syntaxelemente, die vor allem aus historischen Gründen hinzugefügt wurden. Siehe Punkt 72.)

Dies ist zum Beispiel ein gültiges TypeScript-Programm:

function greet(who: string) {
  console.log('Hello', who);
}

Aber wenn du das durch ein Programm wie node laufen lässt, das JavaScript erwartet, bekommst du einen Fehler:

function greet(who: string) {
                  ^

SyntaxError: Unexpected token :

Die : string ist eine Typ-Annotation, die spezifisch für TypeScript ist. Sobald du eine solche Annotation verwendest, gehst du über einfaches JavaScript hinaus (siehe Abbildung 1-1).

ets2 0101
Abbildung 1-1. Alles JavaScript ist TypeScript, aber nicht alles TypeScript ist JavaScript.

Das soll nicht heißen, dass TypeScript keine Vorteile für einfache JavaScript-Programme bietet. Das tut es! Zum Beispiel dieses JavaScript-Programm:

let city = 'new york city';
console.log(city.toUppercase());

wird einen Fehler auslösen, wenn du es ausführst:

TypeError: city.toUppercase is not a function

In diesem Programm gibt es keine Typ-Annotationen, aber der TypeScript-Typ-Checker kann das Problem trotzdem erkennen:

let city = 'new york city';
console.log(city.toUppercase());
//               ~~~~~~~~~~~ Property 'toUppercase' does not exist on type
//                           'string'. Did you mean 'toUpperCase'?

Du musst TypeScript nicht sagen, dass der Typ von city string ist: Es leitet ihn aus dem Anfangswert von ab. Die Typinferenz ist ein wichtiger Bestandteil von TypeScript, und in Kapitel 3 erfährst du, wie du sie gut nutzen kannst.

Eines der Ziele des TypeScript-Typensystems ist es, Code zu erkennen, der zur Laufzeit eine Ausnahme auslöst, ohne dass du deinen Code ausführen musst. Wenn TypeScript als "statisches" Typsystem bezeichnet wird, bezieht sich das auf diese Fähigkeit. Der Typprüfer kann Code, der Ausnahmen auslöst, nicht immer erkennen, aber er wird es versuchen.

Auch wenn dein Code keine Ausnahme auslöst, kann es sein, dass er nicht das tut, was du beabsichtigst. TypeScript versucht, auch einige dieser Probleme abzufangen. Zum Beispiel diesesJavaScript-Programm:

const states = [
  {name: 'Alabama', capital: 'Montgomery'},
  {name: 'Alaska',  capital: 'Juneau'},
  {name: 'Arizona', capital: 'Phoenix'},
  // ...
];
for (const state of states) {
  console.log(state.capitol);
}

wird protokolliert:

undefined
undefined
undefined

Huch! Was ist falsch gelaufen? Dieses Programm ist gültiges JavaScript (und damit TypeScript). Und es lief ohne Fehler. Aber es hat eindeutig nicht das getan, was du beabsichtigt hast. Auch ohne das Hinzufügen von Typ-Annotationen kann der TypeScript-Typ-Checker den Fehler erkennen und dir einen hilfreichen Vorschlag machen:

for (const state of states) {
  console.log(state.capitol);
  //                ~~~~~~~ Property 'capitol' does not exist on type
  //                        '{ name: string; capital: string; }'.
  //                        Did you mean 'capital'?
}

Wir meinten in der Tat capital mit einem "a". Staaten und Länder haben Hauptstädte ("a"), während die Parlamente in Kapitolgebäuden ("o") tagen.

TypeScript kann zwar auch Fehler finden, wenn du keine Typ-Annotationen angibst, aber wenn du sie angibst, ist es in der Lage, eine viel gründlichere Arbeit zu leisten. Das liegt daran, dass TypeScript durch die Typ-Annotationen erfährt, was deine Absicht ist, und dadurch kann es erkennen, wo das Verhalten deines Codes nicht mit deiner Absicht übereinstimmt. Was wäre zum Beispiel, wenn du den Rechtschreibfehler capital/capitol im vorherigen Beispiel rückgängig gemacht hättest?

const states = [
  {name: 'Alabama', capitol: 'Montgomery'},
  {name: 'Alaska',  capitol: 'Juneau'},
  {name: 'Arizona', capitol: 'Phoenix'},
  // ...
];
for (const state of states) {
  console.log(state.capital);
  //                ~~~~~~~ Property 'capital' does not exist on type
  //                        '{ name: string; capitol: string; }'.
  //                        Did you mean 'capitol'?
}

Der Fehler, der vorher so hilfreich war, ist jetzt genau falsch! Das Problem ist, dass du dieselbe Eigenschaft auf zwei verschiedene Arten geschrieben hast, und TypeScript weiß nicht, welche richtig ist. Es kann zwar raten, aber das ist nicht immer richtig. Die Lösung ist, dass du deine Absicht klarstellst, indem du den Typ von states explizit deklarierst:

interface State {
  name: string;
  capital: string;
}
const states: State[] = [
  {name: 'Alabama', capitol: 'Montgomery'},
  //                ~~~~~~~
  {name: 'Alaska',  capitol: 'Juneau'},
  //                ~~~~~~~
  {name: 'Arizona', capitol: 'Phoenix'},
  //                ~~~~~~~ Object literal may only specify known properties,
  //                        but 'capitol' does not exist in type 'State'.
  //                        Did you mean to write 'capital'?
  // ...
];
for (const state of states) {
  console.log(state.capital);
}

Jetzt stimmen die Fehler mit dem Problem überein und die vorgeschlagene Lösung ist korrekt. Indem du deine Absicht klargestellt hast, hast du TypeScript auch geholfen, andere potenzielle Probleme zu erkennen. Hättest du z. B. capitol nur einmal im Array falsch geschrieben, hätte es vorher keinen Fehler gegeben. Mit der Type-Annotation ist das aber der Fall:

const states: State[] = [
  {name: 'Alabama', capital: 'Montgomery'},
  {name: 'Alaska',  capitol: 'Juneau'},
  //                ~~~~~~~ Did you mean to write 'capital'?
  {name: 'Arizona', capital: 'Phoenix'},
  // ...
];

Das wird eine vertraute Dynamik, wenn du mit dem Typprüfer arbeitest: Je mehr Informationen du ihm gibst, desto mehr Probleme wird er finden können.

Im Hinblick auf das Venn-Diagramm können wir eine neue Gruppe von Programmen hinzufügen: TypeScript-Programme, die die Typprüfung bestehen (siehe Abbildung 1-2).

ets2 0102
Abbildung 1-2. Alle JavaScript-Programme sind TypeScript-Programme. Aber nur einige JavaScript- (und TypeScript-) Programme bestehen den Type-Checker.

Wenn dir die Aussage "TypeScript ist eine Obermenge von JavaScript" falsch vorkommt, liegt das vielleicht daran, dass du an die dritte Gruppe von Programmen im Diagramm denkst. In der Praxis ist dies der wichtigste Satz für die tägliche Erfahrung mit TypeScript. Wenn du TypeScript verwendest, versuchst du in der Regel, dass dein Code alle Typprüfungen besteht.

Das Typsystem von TypeScript modelliert das Laufzeitverhalten von JavaScript. Das kann zu einigen Überraschungen führen, wenn du aus einer Sprache mit strengeren Laufzeitprüfungen kommst. Ein Beispiel:

const x = 2 + '3';  // OK
//    ^? const x: string
const y = '2' + 3;  // OK
//    ^? const y: string

Diese Anweisungen bestehen beide die Typprüfung, auch wenn sie fragwürdig sind und in vielen anderen Sprachen Laufzeitfehler produzieren. Aber dies bildet das Laufzeitverhalten von JavaScript genau ab, wo beide Ausdrücke die Zeichenkette "23" ergeben.

TypeScript zieht jedoch irgendwo die Grenze. Der Typprüfer bemerkt Probleme in all diesen Anweisungen, auch wenn sie zur Laufzeit keine Ausnahmen auslösen:

const a = null + 7;  // Evaluates to 7 in JS
//        ~~~~ The value 'null' cannot be used here.
const b = [] + 12;  // Evaluates to '12' in JS
//        ~~~~~~~ Operator '+' cannot be applied to types ...
alert('Hello', 'TypeScript');  // alerts "Hello"
//             ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2

Das Leitprinzip des TypeScript-Typensystems ist, dass es das Laufzeitverhalten von JavaScript modellieren soll. Aber in all diesen Fällen hält TypeScript es für wahrscheinlicher, dass die seltsame Verwendung das Ergebnis eines Fehlers ist als die Absicht des Entwicklers, also geht es über die einfache Modellierung des Laufzeitverhaltens hinaus. Ein weiteres Beispiel dafür haben wir im Beispiel capital/capitol gesehen, bei dem das Programm zwar keine Fehlermeldung auslöste (es protokollierte undefined), die Typüberprüfung aber trotzdem einen Fehler anzeigte.

Wie entscheidet TypeScript, wann es das Laufzeitverhalten von JavaScript modelliert und wann es darüber hinausgeht? Letztlich ist das eine Frage des Geschmacks. Wenn du dich für TypeScript entscheidest, vertraust du auf das Urteil des Teams, das die Software entwickelt. Wenn du gerne null und 7 oder [] und 12 hinzufügst oder Funktionen mit überflüssigen Argumenten aufrufst, dann ist TypeScript vielleicht nichts für dich!

Wenn dein Programm den Typ prüft, könnte es trotzdem zur Laufzeit einen Fehler auslösen? Die Antwort ist "ja". Hier ist ein Beispiel:

const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase());

Wenn du das ausführst, wird es ausgelöst:

TypeError: Cannot read properties of undefined (reading 'toUpperCase')

TypeScript nahm an, dass der Array-Zugriff innerhalb der Grenzen liegen würde, aber das war nicht der Fall. Das Ergebnis war eine Ausnahme.

Unerwartete Fehler treten auch häufig auf, wenn du den Typ any verwendest, was wir in Punkt 5 und in Kapitel 5 genauer besprechen werden.

Die Ursache für diese Ausnahmen ist, dass TypeScript den Typ eines Wertes (seinen statischen Typ) und seinen tatsächlichen Typ zur Laufzeit nicht richtig verstanden hat. Ein Typsystem, das die Genauigkeit seiner statischen Typen garantieren kann, gilt als solide. TypeScripts Typsystem ist weder solide, noch war es jemals dafür gedacht. In Artikel 48 werden weitere Möglichkeiten aufgezeigt, wie Unstimmigkeiten entstehen können.

Wenn dir die Sicherheit wichtig ist, solltest du dir andere Sprachen wie Reason, PureScript oder Dart ansehen. Diese Sprachen bieten zwar mehr Garantien für die Sicherheit zur Laufzeit, aber das hat seinen Preis: Es ist mehr Arbeit, ihre Typprüfer davon zu überzeugen, dass dein Code korrekt ist, und keine dieser Sprachen ist eine Obermenge von JavaScript, sodass die Migration komplizierter ist.

Dinge zum Erinnern

  • TypeScript ist eine Obermenge von JavaScript: Alle JavaScript-Programme sind syntaktisch gültige TypeScript-Programme, aber nicht alle TypeScript-Programme sind gültige JavaScript-Programme.

  • TypeScript fügt ein statisches Typsystem hinzu, das das Laufzeitverhalten von JavaScript modelliert und versucht, Code zu erkennen, der zur Laufzeit Ausnahmen auslöst.

  • Es ist möglich, dass der Code die Typprüfung besteht, aber trotzdem zur Laufzeit ausfällt.

  • TypeScript verbietet einige legale, aber fragwürdige JavaScript-Konstrukte wie den Aufruf von Funktionen mit der falschen Anzahl von Argumenten.

  • Typ-Annotationen teilen TypeScript deine Absicht mit und helfen ihm, korrekten und falschen Code zu unterscheiden.

Punkt 2: Wisse, welche TypeScript-Optionen du verwendest

Besteht dieser Code den Typprüfer?

function add(a, b) {
  return a + b;
}
add(10, null);

Ohne zu wissen, welche Optionen du verwendest, ist es unmöglich, das zu sagen! Der TypeScript-Compiler verfügt über eine enorme Anzahl von Optionen, über hundert zum Zeitpunkt der Erstellung dieses Artikels.

Sie können über die Kommandozeile gesetzt werden:

$ tsc --noImplicitAny program.ts

oder über eine Konfigurationsdatei, tsconfig.json:

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

Du solltest die Konfigurationsdatei bevorzugen. Sie stellt sicher, dass deine Mitarbeiter und Werkzeuge genau wissen, wie du TypeScript verwenden willst. Du kannst eine Konfigurationsdatei erstellen, indem du tsc --init.

Viele der TypeScript-Konfigurationseinstellungen steuern, wo die Sprache nach Quelldateien sucht und welche Art von Ausgabe sie erzeugt. Einige wenige steuern jedoch zentrale Aspekte der Sprache selbst. Dies sind Entscheidungen auf höchster Ebene, die die meisten Sprachen ihren Nutzern nicht überlassen. TypeScript kann sich wie eine ganz andere Sprache anfühlen, je nachdem, wie es konfiguriert ist. Um sie effektiv zu nutzen, solltest du die wichtigsten dieser Einstellungen verstehen: noImplicitAny und strictNullChecks.

noImplicitAny

noImplicitAny steuert was TypeScript tut, wenn es den Typ einer Variablen nicht bestimmen kann. Dieser Code ist gültig, wenn noImplicitAny ausgeschaltet ist:

function add(a, b) {
  return a + b;
}

Wenn du in deinem Editor mit der Maus über das Symbol add fährst, siehst du, was TypeScript über den Typ der Funktion herausgefunden hat:

function add(a: any, b: any): any

Die any Typen deaktivieren die Typprüfung für Code mit diesen Parametern. any ist ein nützliches Werkzeug, sollte aber mit Vorsicht verwendet werden. Mehr über any findest du unter Punkt 5 und in Kapitel 5.

Diese werden implizite any s genannt, weil du das Wort "any" nie geschrieben hast, aber trotzdem gefährliche any Typen erhalten hast. Das wird ein Fehler, wenn du die Option noImplicitAny setzt:

function add(a, b) {
  //         ~    Parameter 'a' implicitly has an 'any' type
  //            ~ Parameter 'b' implicitly has an 'any' type
  return a + b;
}

Diese Fehler können behoben werden, indem du explizit Typendeklarationen schreibst, entweder : any oder einen spezifischeren Typ:

function add(a: number, b: number) {
  return a + b;
}

TypeScript ist am hilfreichsten, wenn es über Typinformationen verfügt. Deshalb solltest du, wann immer möglich, noImplicitAny einstellen. Sobald du dich daran gewöhnt hast, dass alle Variablen Typen haben, fühlt sich TypeScript ohne noImplicitAny fast wie eine andere Sprache an.

Bei neuen Projekten solltest du mit noImplicitAny beginnen, so dass du die Typen schreibst, während du deinen Code schreibst. Das hilft TypeScript, Probleme zu erkennen, die Lesbarkeit deines Codes zu verbessern und deine Entwicklungserfahrung zu steigern (siehe Punkt 6).

Die Deaktivierung von noImplicitAny ist nur sinnvoll, wenn du ein Projektvon JavaScript auf TypeScript umstellst (siehe Kapitel 10). Aber auch dann sollte dies nur ein vorübergehender Zustand sein und du solltest ihn so schnell wie möglich wieder aktivieren. TypeScript ohnenoImplicitAny ist erstaunlich locker. In Artikel 83 erfährst du, wie das zu Problemen führen kann.

strictNullChecks

strictNullChecks kontrolliert ob null und undefined in jedem Typ zulässige Werte sind.

Dieser Code ist gültig, wenn strictNullChecks ausgeschaltet ist:

const x: number = null;  // OK, null is a valid number

löst aber einen Fehler aus, wenn du strictNullChecks einschaltest:

const x: number = null;
//    ~ Type 'null' is not assignable to type 'number'

Ein ähnlicher Fehler wäre aufgetreten, wenn du undefined anstelle von null verwendet hättest.

Wenn du beabsichtigst, null zu erlauben, kannst du den Fehler beheben, indem du deine Absicht explizit machst:

const x: number | null = null;

Wenn du null nicht zulassen willst, musst du herausfinden, woher das Geld kommt und entweder einen Scheck oder eine Erklärung hinzufügen:

const statusEl = document.getElementById('status');
statusEl.textContent = 'Ready';
// ~~~~~ 'statusEl' is possibly 'null'.

if (statusEl) {
  statusEl.textContent = 'Ready';  // OK, null has been excluded
}
statusEl!.textContent = 'Ready';  // OK, we've asserted that el is non-null

Die Verwendung einer if Anweisung auf diese Weise wird als "Verengung" oder "Verfeinerung" eines Typs bezeichnet, und dieses Muster wird in Punkt 22 untersucht. Das "!" in der letzten Zeile wird als "Non-Null-Assertion" bezeichnet. Type Assertions haben ihren Platz in TypeScript, aber sie können auch zu Laufzeitausnahmen führen. In Punkt 9 wird erklärt, wann du eine Typ-Assertion verwenden solltest und wann nicht.

strictNullChecks ist enorm hilfreich, um Fehler mit null und undefined Werten zu finden, aber es erhöht die Schwierigkeit, die Sprache zu verwenden. Wenn du ein neues Projekt beginnst und bereits TypeScript benutzt hast, solltest du strictNullChecks einstellen. Wenn du aber neu in der Sprache bist oder eine JavaScript-Codebasis migrierst, kannst du die Option auchweglassen. Du solltest auf jeden Fall noImplicitAny setzen, bevor dustrictNullChecks setzt.

Wenn du dich dafür entscheidest, ohne strictNullChecks zu arbeiten, halte Ausschau nach dem gefürchteten Laufzeitfehler "undefined is not an object". Jeder dieser Fehler ist eine Erinnerung daran, dass du eine strengere Überprüfung aktivieren solltest. Das Ändern dieser Einstellung wird nur schwieriger, wenn dein Projekt wächst, also warte nicht zu lange mit der Aktivierung. Der meiste TypeScript-Code verwendet strictNullChecks, und das ist schließlich der Punkt, an dem du sein willst.

Andere Optionen

Es gibt noch viele andere Einstellungen, die sich auf die Sprachsemantik auswirken (z.B. noImplicitThis und strictFunctionTypes), aber diese sind im Vergleich zu noImplicitAny und strictNullChecks unbedeutend. Um alle diese Prüfungen zu aktivieren, schalte die Einstellung strict ein. TypeScript kann die meisten Fehler mit strict abfangen, daher sollte dies dein Ziel sein.

Wenn du ein Projekt mit tsc --init erstellst, bist du standardmäßig im strict Modus.

Es gibt auch ein paar "strengere als strenge" Optionen. Du kannst diese Optionen wählen, um TypeScript noch aggressiver bei der Fehlersuche in deinem Code zu machen. Eine dieser Optionen ist noUncheckedIndexedAccess, die hilft, Fehler beim Zugriff auf Objekte und Arrays abzufangen. Dieser Code weist zum Beispiel unter --strict keine Typfehler auf, löst aber zur Laufzeit eine Ausnahme aus:

const tenses = ['past', 'present', 'future'];
tenses[3].toUpperCase();

Wenn noUncheckedIndexedAccess eingestellt ist, ist dies ein Fehler:

const tenses = ['past', 'present', 'future'];
tenses[3].toUpperCase();
// ~~~~~~ Object is possibly 'undefined'.

Dies ist jedoch kein kostenloses Mittagessen. Viele gültige Zugänge werden auch als möglicherweise undefined gekennzeichnet:

tenses[0].toUpperCase();
// ~~~~~~ Object is possibly 'undefined'.

Einige TypeScript-Projekte verwenden diese Einstellung, andere nicht. Du solltest dir zumindest bewusst sein, dass es sie gibt. Mehr über diese Einstellung erfährst du in Punkt 48.

Wisse, welche Optionen du verwendest! Wenn ein Kollege oder eine Kollegin ein TypeScript-Beispiel mit dir teilt und du die Fehler nicht reproduzieren kannst, vergewissere dich, dass deine Compiler-Optionen dieselben sind.

Dinge zum Erinnern

  • Der TypeScript-Compiler enthält mehrere Einstellungen, die zentrale Aspekte der Sprache betreffen.

  • Konfiguriere TypeScript mit tsconfig.json anstatt mit Kommandozeilenoptionen.

  • Aktiviere noImplicitAny, es sei denn, du stellst ein JavaScript-Projekt aufTypeScript um.

  • Verwende strictNullChecks, um Laufzeitfehler im Stil von "undefined is not an object" zu vermeiden.

  • Ziel ist es, strict die gründlichste Prüfung zu ermöglichen, die TypeScript bieten kann.

Punkt 3: Verstehen, dass die Codegenerierung unabhängig von Typen ist

Auf einer hohen Ebene macht tsc (der TypeScript-Compiler) zwei Dinge:

  • Es wandelt TypeScript/JavaScript der nächsten Generation in eine ältere Version von JavaScript um, die in Browsern oder anderen Laufzeiten funktioniert ("Transpilierung").

  • Sie prüft deinen Code auf Typfehler.

Erstaunlich ist, dass diese beiden Verhaltensweisen völlig unabhängig voneinander sind. Anders ausgedrückt: Die Typen in deinem Code haben keinen Einfluss auf das JavaScript, das TypeScript ausgibt. Da es dieses JavaScript ist, das ausgeführt wird, bedeutet das, dass deine Typen keinen Einfluss darauf haben, wie dein Code läuft.

Das hat einige überraschende Auswirkungen und sollte deine Erwartungen darüber beeinflussen, was TypeScript für dich tun kann und was nicht.

Du kannst TypeScript-Typen nicht zur Laufzeit prüfen

Du könntest versucht sein, einen Code wie diesen zu schreiben:

interface Square {
  width: number;
}
interface Rectangle extends Square {
  height: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    //                 ~~~~~~~~~ 'Rectangle' only refers to a type,
    //                           but is being used as a value here
    return shape.height * shape.width;
    //           ~~~~~~ Property 'height' does not exist on type 'Shape'
  } else {
    return shape.width * shape.width;
  }
}

Die Prüfung von instanceof erfolgt zur Laufzeit, aber Rectangle ist ein Typ und kann daher das Laufzeitverhalten des Codes nicht beeinflussen. TypeScript-Typen sind "löschbar": Ein Teil der Kompilierung zu JavaScript besteht darin, alle interfaces, types und Typ-Annotationen aus deinem Code zu entfernen. Das ist am einfachsten zu erkennen, wenn du dir das JavaScript ansiehst, zu dem dieses Beispiel kompiliert wird:

function calculateArea(shape) {
  if (shape instanceof Rectangle) {
    return shape.height * shape.width;
  } else {
    return shape.width * shape.width;
  }
}

Hier wird Rectangle vor der instanceof Prüfung nicht erwähnt, daher das Problem.2 Um herauszufinden, um welchen Typ von Form es sich handelt, brauchst du eine Möglichkeit, den Typ zur Laufzeit zu rekonstruieren, d.h. eine Möglichkeit, die im generierten JavaScript und nicht nur im ursprünglichen TypeScript Sinn macht.

Es gibt mehrere Möglichkeiten, dies zu tun. Eine davon ist, auf das Vorhandensein einer heightEigenschaft zu prüfen:

function calculateArea(shape: Shape) {
  if ('height' in shape) {
    return shape.width * shape.height;
    //     ^? (parameter) shape: Rectangle
  } else {
    return shape.width * shape.width;
  }
}

Das funktioniert, weil die Eigenschaftsprüfung nur Werte umfasst, die zur Laufzeit verfügbar sind, aber trotzdem erlaubt, dass der Typchecker den Typ von shapeauf Rectangle verfeinert.

Eine andere Möglichkeit wäre, ein "Tag" einzuführen, um den Typ explizit so zu speichern, dass er zur Laufzeit verfügbar ist:

interface Square {
  kind: 'square';
  width: number;
}
interface Rectangle {
  kind: 'rectangle';
  height: number;
  width: number;
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape.kind === 'rectangle') {
    return shape.width * shape.height;
    //     ^? (parameter) shape: Rectangle
  } else {
    return shape.width * shape.width;
    //     ^? (parameter) shape: Square
  }
}

Hier fungiert die Eigenschaft kind als "Tag", und wir sagen, dass der Typ Shape eine "getaggte Vereinigung" ist. Manchmal wird sie auch als "diskriminierte Vereinigung" bezeichnet, in diesem Fall ist kind die "Diskriminante". Die Begriffe sind austauschbar. Weil sie es so einfach machen, Typinformationen zur Laufzeit wiederherzustellen, sind "tagged/discriminated unions" in TypeScript allgegenwärtig.

Einige Konstrukte führen sowohl einen Typ (der zur Laufzeit nicht verfügbar ist) als auch einen Wert (der verfügbar ist) ein. Das Schlüsselwort class ist eines davon. Eine andere Möglichkeit, den Fehler zu beheben, wäre, Square und Rectangle zu Klassen zu machen:

class Square {
  width: number;
  constructor(width: number) {
    this.width = width;
  }
}
class Rectangle extends Square {
  height: number;
  constructor(width: number, height: number) {
    super(width);
    this.height = height;
  }
}
type Shape = Square | Rectangle;

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    return shape.width * shape.height;
    //     ^? (parameter) shape: Rectangle
  } else {
    return shape.width * shape.width;
    //     ^? (parameter) shape: Square
  }
}

Das funktioniert, weil class Rectangle sowohl einen Typ als auch einen Wert einführt, während interface nur einen Typ einführt.

Das Rectangle in type Shape = Square | Rectangle bezieht sich auf den Typ, aber das Rectangle in shape instanceof Rectangle bezieht sich auf den Wert, in diesem Fall die Konstruktorfunktion. Diese Unterscheidung ist wichtig zu verstehen, kann aber ziemlich subtil sein. Punkt 8 zeigt dir, wie du unterscheiden kannst.

Code mit Tippfehlern kann Ausgaben erzeugen

Da die Codeausgabe unabhängig von der Typüberprüfung ist, folgt daraus, dass Code mit Typfehlern eine Ausgabe erzeugen kann!

$ cat test.ts
let x = 'hello';
x = 1234;
$ tsc test.ts
test.ts:2:1 - error TS2322: Type '1234' is not assignable to type 'string'

2 x = 1234;
  ~

$ cat test.js
var x = 'hello';
x = 1234;

Das kann ziemlich überraschend sein, wenn du aus einer Sprache wie C oder Java kommst, wo Typprüfung und Ausgabe Hand in Hand gehen. Du kannst dir alle TypeScript-Fehler ähnlich wie Warnungen in diesen Sprachen vorstellen: Es ist wahrscheinlich, dass sie auf ein Problem hinweisen und eine Untersuchung wert sind, aber sie werden den Build nicht stoppen.

Code-Emissionen bei Vorliegen von Fehlern sind in der Praxis hilfreich. Wenn du eine Webanwendung entwickelst, weißt du vielleicht, dass es Probleme mit einem bestimmten Teil der Anwendung gibt. Da TypeScript aber auch bei Fehlern Code erzeugt, kannst du die anderen Teile deiner Anwendung testen, bevor du sie reparierst.

Du solltest beim Übertragen des Codes möglichst keine Fehler machen, damit du dich nicht daran erinnern musst, was ein erwarteter oder unerwarteter Fehler ist. Wenn du die Ausgabe von Fehlern deaktivieren möchtest, kannst du die Option noEmitOnError in tsconfig.json oder das Äquivalent in deinem Build-Tool verwenden.

Typoperationen können keine Laufzeitwerte beeinflussen

Angenommen, du hast einen Wert, der sowohl eine Zeichenkette als auch eine Zahl sein kann, und du möchtest ihn so normalisieren, dass er immer eine Zahl ist. Hier ist ein fehlgeleiteter Versuch, den der Typprüfer akzeptiert:

function asNumber(val: number | string): number {
  return val as number;
}

Wenn du dir das generierte JavaScript ansiehst, wird klar, was diese Funktion wirklich tut:

function asNumber(val) {
  return val;
}

Es findet überhaupt keine Umwandlung statt. as number ist eine Typ-Operation und kann daher das Laufzeitverhalten deines Codes nicht beeinflussen. Um den Wert zu normalisieren, musst du seinen Laufzeittyp überprüfen und die Konvertierung mithilfe von JavaScript-Konstrukten vornehmen:

function asNumber(val: number | string): number {
  return Number(val);
}

"as number" ist eine Typbehauptung, die manchmal ungenau als "Cast" bezeichnet wird. Mehr darüber, wann es sinnvoll ist, Typ-Assertions zu verwenden, findest du unter Punkt 9.

Laufzeittypen dürfen nicht mit deklarierten Typen identisch sein

Könnte diese Funktion jemals die endgültige console.log treffen?

function setLightSwitch(value: boolean) {
  switch (value) {
    case true:
      turnLightOn();
      break;
    case false:
      turnLightOff();
      break;
    default:
      console.log(`I'm afraid I can't do that.`);
  }
}

Normalerweise meldet TypeScript toten Code, aber es beschwert sich nicht darüber, auch nicht mit der Option strict. Wie konntest du diesen Zweig treffen?

Das Wichtigste ist, dass du dich daran erinnerst, dass boolean der deklarierte Typ ist. Da es ein TypeScript-Typ ist, verschwindet er zur Laufzeit. Im JavaScript-Code könnte ein Benutzer versehentlich einensetLightSwitch mit einem Wert wie "ON" aufrufen.

Es gibt auch Möglichkeiten, diesen Codepfad in reinem TypeScript auszulösen. Vielleicht wird die Funktion mit einem Wert aufgerufen, der von einem Netzwerkaufruf stammt:

interface LightApiResponse {
  lightSwitchValue: boolean;
}
async function setLight() {
  const response = await fetch('/light');
  const result: LightApiResponse = await response.json();
  setLightSwitch(result.lightSwitchValue);
}

Du hast angegeben, dass das Ergebnis der /light Anfrage LightApiResponse ist, aber nichts erzwingt dies. Wenn du die API falsch verstanden hast und lightSwitchValue in Wirklichkeit ein string ist, wird zur Laufzeit ein String an setLightSwitch übergeben. Oder vielleicht hat sich die API geändert, nachdem du sie eingesetzt hast.

TypeScript kann ziemlich verwirrend sein, wenn deine Laufzeittypen nicht mit den deklarierten Typen übereinstimmen. Du solltest diese so genannten "unsound" Typen vermeiden, wann immer du kannst. Aber sei dir bewusst, dass es möglich ist, dass ein Wert einen anderen Laufzeittyp hat als den, den du deklariert hast. Unter findest du weitere Informationen über Soundness, siehe Punkt 48.

Du kannst eine Funktion, die auf TypeScript-Typen basiert, nicht überladen

Sprachen wie C++ ermöglichen es dir, mehrere Versionen einer Funktion zu definieren, die sich nur durch die Typen ihrer Parameter unterscheiden. Das nennt man "Funktionsüberladung". Da das Laufzeitverhalten deines Codes unabhängig von seinen TypeScript-Typen ist, ist dieses Konstrukt in TypeScript nicht möglich:

function add(a: number, b: number) { return a + b; }
//       ~~~ Duplicate function implementation
function add(a: string, b: string) { return a + b; }
//       ~~~ Duplicate function implementation

TypeScript bietet zwar die Möglichkeit, Funktionen zu überladen, aber das geschieht ausschließlich auf der Typebene. Du kannst mehrere Typsignaturen für eine Funktion angeben, aber nur eine einzige Implementierung:

function add(a: number, b: number): number;
function add(a: string, b: string): string;

function add(a: any, b: any) {
  return a + b;
}

const three = add(1, 2);
//    ^? const three: number
const twelve = add('1', '2');
//    ^? const twelve: string

Die ersten beiden Signaturen von add liefern nur Typinformationen. Wenn TypeScript eine JavaScript-Ausgabe erzeugt, werden sie entfernt und es bleibt nur die Implementierung übrig. Die any Parameter in der Implementierung sind nicht besonders gut. Wie du damit umgehst, erfährst du in Artikel 52, in dem es auch um einige Feinheiten geht, die bei TypeScript-Funktionsüberladungen zu beachten sind.

TypeScript-Typen haben keinen Einfluss auf die Laufzeitleistung

Da Typen und Typoperationen beim Generieren von JavaScript gelöscht werden, können sie sich nicht auf die Laufzeitleistung auswirken. Die statischen Typen von TypeScript sind wirklich kostenlos. Wenn das nächste Mal jemand Laufzeit-Overhead als Grund anführt, TypeScript nicht zu verwenden, wirst du genau wissen, wie gut er diese Behauptung getestet hat!

Dabei gibt es zwei Vorbehalte:

  • Während es keinen Laufzeit-Overhead gibt, verursacht der TypeScript-Compiler einen Overhead bei der Build-Zeit. Das TypeScript-Team nimmt die Leistung des Compilers sehr ernst, und die Kompilierung ist in der Regel recht schnell, vor allem bei inkrementellen Builds. Wenn der Mehraufwand zu groß wird, kann dein Build-Tool die Option "Transpile only" anbieten, um die Typüberprüfung zu überspringen. Mehr über die Leistung des Compilers erfährst du in Artikel 78.

  • Der Code, den TypeScript ausgibt, um ältere Laufzeiten zu unterstützen , kann im Vergleich zu nativen Implementierungen einen Leistungszuschlag bedeuten. Wenn du z. B. Generatorfunktionen verwendest und auf ES5 abzielst, das vor den Generatoren liegt, gibt tsc Hilfscode aus, damit alles funktioniert. Dadurch entsteht ein gewisser Overhead im Vergleich zu einer nativen Implementierung von Generatoren. Das ist bei jedem JavaScript-"Transpiler" der Fall, nicht nur bei TypeScript. Unabhängig davon hat dies mit dem Emit-Ziel und der Sprachebene zu tun und ist immer noch unabhängig von .

Dinge zum Erinnern

  • Die Codegenerierung ist unabhängig vom Typsystem. Das bedeutet, dass TypeScript-Typen keinen Einfluss auf das Laufzeitverhalten deines Codes haben.

  • Es ist möglich, dass ein Programm mit Tippfehlern Code erzeugt ("kompiliert").

  • TypeScript-Typen sind zur Laufzeit nicht verfügbar. Um einen Typ zur Laufzeit abzufragen, brauchst du eine Möglichkeit, ihn zu rekonstruieren. Tagged Unions und Property Checking sind gängige Methoden, um dies zu tun.

  • Einige Konstrukte, wie z.B. class, führen sowohl einen TypeScript-Typ als auch einen Wert ein, der zur Laufzeit verfügbar ist.

  • Da sie bei der Kompilierung gelöscht werden, können TypeScript-Typen die Laufzeitleistung von deinem Code nicht beeinflussen.

Punkt 4: Mach dich mit der Strukturtypisierung vertraut

JavaScript fördert das "Duck Typing": Wenn du einer Funktion einen Wert mit den richtigen Eigenschaften übergibst, ist es ihr egal, wie du den Wert erstellt hast. Sie wird ihn einfach verwenden. (Dieser Begriff bezieht sich auf das Sprichwort: "Wenn es wie eine Ente läuft und wie eine Ente spricht, dann ist es wahrscheinlich eine Ente").

TypeScript modelliert dieses Verhalten durch ein sogenanntes strukturelles Typsystem. Das kann manchmal zu überraschenden Ergebnissen führen, weil der Typprüfer ein anderes Verständnis von einem Typ hat, als du es dir vorgestellt hast. Ein gutes Verständnis der strukturellen Typisierung hilft dir, Fehler und Nicht-Fehler zu erkennen und robusteren Code zu schreiben.

Angenommen, du arbeitest an einer Physikbibliothek und hast einen 2D-Vektortyp:

interface Vector2D {
  x: number;
  y: number;
}

Du schreibst eine Funktion, um ihre Länge zu berechnen:

function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x ** 2 + v.y ** 2);
}

Jetzt führst du den Begriff des benannten Vektors ein:

interface NamedVector {
  name: string;
  x: number;
  y: number;
}

Die Funktion calculateLength funktioniert mit NamedVectors, weil sie x und y Eigenschaften haben, die numbers sind. TypeScript ist schlau genug, das herauszufinden:

const v: NamedVector = { x: 3, y: 4, name: 'Pythagoras' };
calculateLength(v);  // OK, result is 5

Interessant ist, dass du die Beziehung zwischen Vector2D und NamedVector nie erklärt hast. Und du musstest auch nicht eine alternative Implementierung von calculateLength für NamedVectors schreiben. Das Typsystem von TypeScript modelliert das Laufzeitverhalten von JavaScript(Punkt 1). Es ermöglichte den Aufruf von calculateLength mit einemNamedVector aufgerufen werden, weil seine Struktur mit Vector2D kompatibel war. Daher kommt auch der Begriff "strukturelle Typisierung".

Aber das kann auch zu Problemen führen. Angenommen, du fügst einen 3D-Vektortyp hinzu:

interface Vector3D {
  x: number;
  y: number;
  z: number;
}

und schreibe eine Funktion, um sie zu normalisieren (ihre Länge auf 1 zu setzen):

function normalize(v: Vector3D) {
  const length = calculateLength(v);
  return {
    x: v.x / length,
    y: v.y / length,
    z: v.z / length,
  };
}

Wenn du diese Funktion aufrufst, wirst du wahrscheinlich einen Vektor mit einer Länge größer als 1 erhalten:

> normalize({x: 3, y: 4, z: 5})
{ x: 0.6, y: 0.8, z: 1 }

Dieser Vektor hat eine Länge von etwa 1,4 und nicht 1. Was ist also schief gelaufen und warum hat TypeScript den Fehler nicht erkannt?

Der Fehler ist, dass calculateLength mit 2D-Vektoren arbeitet, normalize aber mit 3D-Vektoren. Daher wird die Komponente z bei der Normalisierung ignoriert.

Was vielleicht noch überraschender ist, ist, dass der Typprüfer dieses Problem nicht erkennt. Warum darfst du calculateLength mit einem 3D-Vektor aufrufen, obwohl die Typdeklaration besagt, dass sie 2D-Vektoren akzeptiert?

Was bei benannten Vektoren so gut funktioniert hat, geht hier nach hinten los. Der Aufruf von calculateLength mit einem {x, y, z} Objekt führt nicht zu einem Fehler. Der Typprüfer beschwert sich also auch nicht, und dieses Verhalten hat zu einem Fehler geführt.

(Wenn du möchtest, dass dies ein Fehler ist, hast du einige Möglichkeiten. In Punkt 63 wird ein Trick vorgestellt, mit dem du die Eigenschaft z gezielt verbieten kannst, und in Punkt 64 wird gezeigt, wie du "Marken" verwenden kannst, um diese Art der strukturellen Typisierung ganz zu verhindern).

Wenn du Funktionen schreibst, kannst du dir leicht vorstellen, dass sie mit Argumenten aufgerufen werden, die die von dir deklarierten Eigenschaften haben und keine anderen. Das ist bekannt als ein "geschlossener", "versiegelter" oder "präziser" Typ und kann im Typsystem von TypeScript nicht ausgedrückt werden. Ob es dir gefällt oder nicht, deine Typen sind "offen".

Das kann manchmal zu Überraschungen führen:

function calculateLengthL1(v: Vector3D) {
  let length = 0;
  for (const axis of Object.keys(v)) {
    const coord = v[axis];
    //            ~~~~~~~ Element implicitly has an 'any' type because ...
    //                    'string' can't be used to index type 'Vector3D'
    length += Math.abs(coord);
  }
  return length;
}

Warum ist das ein Fehler? Da axis einer der Schlüssel von v ist, das ein Vector3D ist, sollte es entweder "x", "y" oder "z" sein. Und laut der Deklaration von Vector3D sind das alles numbers, sollte der Typ von coord also nicht number sein?

Ist dieser Fehler ein falsches Positiv? Nein! TypeScript hat Recht, wenn es sich beschwert. Die Logik im vorherigen Absatz geht davon aus, dass Vector3D versiegelt ist und keine anderen Eigenschaften hat. Das könnte aber der Fall sein:

const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
calculateLengthL1(vec3D);  // OK, returns NaN

Da v jede beliebige Eigenschaft haben kann, ist der Typ von axis string . TypeScript hat keinen Grund zu glauben, dass v[axis] eine Zahl ist, denn wie du gerade gesehen hast, könnte sie es nicht sein. (Die Variable vec3D vermeidet hier eine übermäßige Eigenschaftsprüfung, die Gegenstand von Punkt 11 ist).

Das Iterieren über Objekte kann knifflig sein, wenn man sie richtig tippen will. Wir werden in Punkt 60 auf dieses Thema zurückkommen, aber in diesem Fall wäre eine Implementierung ohne Schleifen besser:

function calculateLengthL1(v: Vector3D) {
  return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}

Strukturelle Typisierung kann auch bei classes zu Überraschungen führen, die strukturell für Zuordenbarkeit verglichen werden:

class SmallNumContainer {
  num: number;
  constructor(num: number) {
    if (num < 0 || num >= 10) {
      throw new Error(`You gave me ${num} but I want something 0-9.`)
    }
    this.num = num;
  }
}

const a = new SmallNumContainer(5);
const b: SmallNumContainer = { num: 2024 };  // OK!

Warum ist b der SmallNumContainer zuzuordnen? Es hat eine num Eigenschaft, die eine number ist. Die Strukturen stimmen also überein. Das kann zu Problemen führen, wenn du eine Funktion schreibst, die davon ausgeht, dass die Validierungslogik im Konstruktor von SmallNumContainerausgeführt wurde. Bei Klassen mit mehreren Eigenschaften und Methoden ist es weniger wahrscheinlich, dass dies zufällig passiert, aber es ist ganz anders als in Sprachen wie C++ oder Java, wo die Deklaration eines Parameters vom Typ SmallNumContainer garantiert, dass er entweder SmallNumContainer oder eine Unterklasse davon ist und somit die Validierungslogik im Konstruktor ausgeführt wurde.

Strukturelle Typisierung ist von Vorteil, wenn du Tests schreibst. Angenommen, du hast eine Funktion, die eine Abfrage in einer Datenbank ausführt und die Ergebnisse verarbeitet:

interface Author {
  first: string;
  last: string;
}
function getAuthors(database: PostgresDB): Author[] {
  const authorRows = database.runQuery(`SELECT first, last FROM authors`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

Um dies zu testen, könntest du eine Attrappe PostgresDB erstellen. Einfacher ist es jedoch, die strukturelle Typisierung zu verwenden und eine engere Schnittstelle zu definieren:

interface DB {
  runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
  const authorRows = database.runQuery(`SELECT first, last FROM authors`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

Du kannst getAuthors trotzdem eine PostgresDB in der Produktion übergeben, da sie eine runQuery Methode hat. Wegen der strukturellen Typisierung muss PostgresDB nicht sagen, dass es DB implementiert. TypeScript findet heraus, dass sie es tut.

Wenn du deine Tests schreibst, kannst du stattdessen ein einfacheres Objekt übergeben:

test('getAuthors', () => {
  const authors = getAuthors({
    runQuery(sql: string) {
      return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
    }
  });
  expect(authors).toEqual([
    {first: 'Toni', last: 'Morrison'},
    {first: 'Maya', last: 'Angelou'}
  ]);
});

TypeScript überprüft, ob unser Test DB mit der Schnittstelle übereinstimmt. Und deine Tests müssen nichts über deine Produktionsdatenbank wissen: keine Mocking-Bibliotheken notwendig! Durch die Einführung einer Abstraktion (DB) haben wir unsere Logik (und die Tests) von den Details einer bestimmten Implementierung (PostgresDB) befreit.

Ein weiterer Vorteil der strukturellen Typisierung ist, dass sie Abhängigkeiten zwischen Bibliotheken sauber trennen kann. Mehr dazu erfährst du unter Punkt 70.

Dinge zum Erinnern

  • Verstehe, dass JavaScript duck typed ist und TypeScript strukturelle Typisierung verwendet, um dies zu modellieren: Werte, die deinen Interfaces zugewiesen werden, können Eigenschaften haben, die über die explizit in deinen Typdeklarationen aufgeführten hinausgehen. Typen sind nicht "versiegelt".

  • Sei dir bewusst, dass Klassen auch strukturellen Typisierungsregeln folgen. Es kann sein, dass du keine Instanz der Klasse hast, die du erwartest!

  • Verwende die strukturelle Typisierung, um Unit-Tests zu erleichtern.

Punkt 5: Beschränke die Verwendung eines beliebigen Typs

Das Typsystem von TypeScript ist schrittweise und optional: schrittweise, weil du deinem Code nach und nach Typen hinzufügen kannst (mit noImplicitAny), und optional, weil du die Typüberprüfung jederzeit deaktivieren kannst. Der Schlüssel zu diesen Funktionen ist der any Typ:

let ageInYears: number;
ageInYears = '12';
// ~~~~~~~ Type 'string' is not assignable to type 'number'.
ageInYears = '12' as any;  // OK

Der Typprüfer hat Recht, wenn er sich hier beschwert, aber du kannst ihn zum Schweigen bringen, indem du as any eingibst. Wenn du anfängst, TypeScript zu benutzen, ist es verlockend, any types und type assertions (as any) zu verwenden, wenn du einen Fehler nicht verstehst, denkst, dass der Typprüfer falsch liegt, oder dir einfach nicht die Zeit nehmen willst, Typendeklarationen zu schreiben.

In manchen Fällen mag das in Ordnung sein, aber sei dir bewusst, dass die Verwendung von any viele der Vorteile von TypeScript eliminiert. Du solltest zumindest die Gefahren verstehen, bevor du es benutzt.

Es gibt keine Typensicherheit bei allen Typen

Im vorangegangenen Beispiel besagt die Typdeklaration, dass ageInYears ein number ist. Aber any erlaubt es dir, ihm ein string zuzuweisen. Der Typprüfer wird glauben, dass es sich um ein number handelt (schließlich hast du das gesagt), und das Chaos bleibt unentdeckt:

ageInYears += 1;  // OK; at runtime, ageInYears is now "121"

jeder lässt dich Verträge brechen

Wenn du eine Funktion schreibst, legst du einen Vertrag fest: Wenn der Aufrufer dir eine bestimmte Art von Eingabe gibt, wirst du eine bestimmte Art von Ausgabe produzieren. Aber mit any kannst du diese Verträge brechen:

function calculateAge(birthDate: Date): number {
  // ...
}

let birthDate: any = '1990-01-19';
calculateAge(birthDate);  // OK

Der Parameter für das Geburtsdatum sollte ein Date sein, nicht ein string. Der Typ any hat dich den Vertrag von calculateAge brechen lassen. Das kann besonders problematisch sein, weil JavaScript oft bereit ist, implizit zwischen Typen zu konvertieren. Ein string funktioniert manchmal dort, wo ein number erwartet wird, nur um unter anderen Umständen zu scheitern.

Es gibt keine Sprachdienste für alle Arten

Wenn eine -Variable einen Typ hat, der nichtany ist, können die TypeScript-Sprachdienste intelligente Autovervollständigung und kontextbezogene Dokumentationbereitstellen (wie in Abbildung 1-3 gezeigt).

ets2 0103
Abbildung 1-3. Der TypeScript Language Service kann kontextbezogene Autovervollständigung für Symbole mit Typen anbieten.

Aber bei Symbolen mit einem any Typ bist du auf dich allein gestellt(Abbildung 1-4).

ets2 0104
Abbildung 1-4. Es gibt keine Autovervollständigung für Eigenschaften von Symbolen mit any Typen.

Das Umbenennen ist ein weiterer solcher Dienst. Wenn du einen Person Typ und Funktionen zum Formatieren des Namens einer Person hast:

interface Person {
  first: string;
  last: string;
}

const formatName = (p: Person) => `${p.first} ${p.last}`;
const formatNameAny = (p: any) => `${p.first} ${p.last}`;

dann kannst du in deinem Editor first auswählen, "Symbol umbenennen" wählen und es in firstName ändern (siehe Abbildungen 1-5 und 1-6).

ets2 0105
Abbildung 1-5. Umbenennen eines Symbols in VS Code.
ets2 0106
Abbildung 1-6. Auswählen des neuen Namens.

Dadurch wird die Funktion formatName geändert, nicht aber die Version any:

interface Person {
  firstName: string;
  last: string;
}
const formatName = (p: Person) => `${p.firstName} ${p.last}`;
const formatNameAny = (p: any) => `${p.first} ${p.last}`;

Einer der Slogans von TypeScript ist "JavaScript, das skaliert". Ein wichtiger Bestandteil von "Skalierung" sind die Sprachdienste, die ein wesentlicher Bestandteil der TypeScript-Erfahrung sind (siehe Punkt 6). Wenn du sie verlierst, bedeutet das einen Produktivitätsverlust, nicht nur für dich, sondern auch für alle anderen, die mit deinem Code arbeiten.

alle Arten von Bugs maskieren, wenn du Code refaktorisierst

Angenommen, du baust eine Webanwendung, in der die Benutzer eine Art von Artikel auswählen können. Eine deiner Komponenten könnte einen onSelectItem Callback haben. Einen Typ für einen Artikel zu schreiben, scheint dir zu mühsam zu sein, also benutzt du einfach any als Ersatz:

interface ComponentProps {
  onSelectItem: (item: any) => void;
}

Hier ist der Code, der diese Komponente verwaltet:

function renderSelector(props: ComponentProps) { /* ... */ }

let selectedId: number = 0;
function handleSelectItem(item: any) {
  selectedId = item.id;
}

renderSelector({onSelectItem: handleSelectItem});

Später überarbeitest du den Selektor so, dass es schwieriger wird, das ganze item Objekt an onSelectItem zu übergeben. Aber das ist nicht weiter schlimm, da du nur die ID brauchst. Du änderst die Signatur in ComponentProps:

interface ComponentProps {
  onSelectItem: (id: number) => void;
}

Du aktualisierst die Komponente und alles besteht die Typprüfung. Sieg!

...oder doch nicht? handleSelectItem nimmt einen any Parameter an, ist also mit einem Item genauso zufrieden wie mit einer ID. Er erzeugt eine Laufzeitausnahme, obwohl er die Typprüfung bestanden hat. Hättest du einen spezifischeren Typ verwendet, wäre dies von der Typüberprüfung erkannt worden.

jeder versteckt dein Schriftdesign

Die Typdefinition für komplexe Objekte, wie den Status deiner Anwendung, kann ziemlich lang werden. Anstatt Typen für die Dutzenden von Eigenschaften im Status deiner Anwendung zu schreiben, könntest du versucht sein, einfach einen any Typ zu verwenden und damit fertig zu sein.

Das ist aus all den Gründen, die in diesem Artikel aufgeführt sind, problematisch. Aber es ist auch deshalb problematisch, weil es das Design deines Status verbirgt. Wie in Kapitel 4 erklärt, ist ein gutes Typendesign wichtig, um sauberen, korrekten und verständlichen Code zu schreiben. Mit einem any Typ ist dein Typdesign implizit. Das macht es schwer zu erkennen, ob das Design gut ist oder ob es überhaupt ein Design gibt. Wenn du einen Kollegen bittest, eine Änderung zu überprüfen, muss er rekonstruieren, ob und wie du den Zustand der Anwendung geändert hast. Besser ist es, wenn du es für alle sichtbar aufschreibst.

jeder untergräbt das Vertrauen in das Schriftsystem

Jedes Mal, wenn du einen Fehler machst und der Typprüfer ihn findet, stärkt das dein Vertrauen in das Typsystem. Aber wenn du zur Laufzeit einen Typfehler siehst, den TypeScript nicht erkannt hat, bekommt dieses Vertrauen einen Dämpfer. Wenn du TypeScript in einem größeren Team einführst, könnten sich deine Kollegen fragen, ob TypeScript die Mühe wert ist. any Typen sind oft die Ursache für diese nicht abgefangenen Fehler.

TypeScript soll dir das Leben leichter machen, aber TypeScript mit vielen any Typen kann schwieriger zu handhaben sein als untypisiertes JavaScript, weil du Typfehler beheben und gleichzeitig die echten Typen im Kopf behalten musst. Wenn deine Typen mit der Realität übereinstimmen, bist du von der Last befreit, die Typinformationen im Kopf behalten zu müssen. TypeScript behält sie für dich im Auge.

Für die Fälle, in denen du any verwenden musst, gibt es bessere und schlechtere Möglichkeiten, es zu tun. Mehr darüber, wie du die Nachteile von any einschränken kannst, erfährst du in Kapitel 5.

Dinge zum Erinnern

  • Mit dem Typ any von TypeScript kannst du die meisten Formen der Typenprüfung für ein Symbol deaktivieren.

  • Der any Typ eliminiert die Typsicherheit, lässt dich Verträge brechen, schadet der Erfahrung der Entwickler, macht das Refactoring fehleranfällig, verbirgt dein Typdesign und untergräbt das Vertrauen in das Typsystem.

  • Vermeide es, any zu benutzen, wenn du kannst!

1 Vielleicht stößt du auf .tsx, .jsx, .mts, .mjs und ein paar andere Erweiterungen. Das sind alles TypeScript- und JavaScript-Dateien.

2 Der beste Weg, um ein Gefühl dafür zu bekommen, ist der TypeScript-Spielplatz, der dein TypeScript und das resultierende JavaScript nebeneinander zeigt.

Get Effektives TypeScript, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.