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 etwas ungewöhnliche Sprache, da sie weder in einem Interpreter läuft (wie Python und Ruby) noch zu einer niedrigeren Sprache kompiliert (wie Java und C). Stattdessen wird sie in eine andere Hochsprache, JavaScript, kompiliert. Es ist dieses JavaScript, das ausgeführt wird, nicht dein TypeScript. Die Beziehung zwischen TypeScript und JavaScript ist also wichtig, kann aber auch zu Verwirrung führen. Wenn du diese Beziehung verstehst, kannst du ein effektiver TypeScript-Entwickler sein.

Das Typsystem von TypeScript hat auch einige ungewöhnliche Aspekte, die du kennen solltest. In späteren Kapiteln wird das Typsystem viel ausführlicher behandelt, aber dieses Kapitel wird dich auf einige Überraschungen aufmerksam machen, die es auf Lager hat.

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.

TypeScript ist eine Obermenge 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 immer noch parsen und JavaScript ausgeben. (Dies ist ein weiterer wichtiger Teil der Beziehung. Darauf gehen wir in Punkt 3 näher ein.)

TypeScript Dateien haben eine .ts (oder .tsx) Endung und nicht die .js (oder .jsx) Endung einer JavaScript Datei. 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.

Diese ist enorm hilfreich, wenn du eine bestehende JavaScript-Codebasis auf TypeScript umstellst. Es bedeutet, dass du deinen Code nicht in einer anderen Sprache neu schreiben musst, um TypeScript zu nutzen und von den Vorteilen zu profitieren. Dies 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 8 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 hinzufügt. (Es gibt noch einige andere Syntaxelemente, die hauptsächlich aus historischen Gründen hinzugefügt wurden. Siehe Punkt 53.)

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 Type-Annotation, die spezifisch für TypeScript ist. Sobald du eine solche Annotation verwendest, bist du über einfaches JavaScript hinausgewachsen (siehe Abbildung 1-1).

efts 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 ab. Die Typinferenz ist ein wichtiger Bestandteil von TypeScript und Kapitel 3 zeigt, wie man sie richtig einsetzt.

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, ist das genau das, worauf es sich bezieht. 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 dieses JavaScript-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'?
}

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 unsere 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. Aber mit der Type Annotation gibt es einen:

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

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).

efts 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, type is string
const y = '2' + 3;  // OK, type is string

Diese Anweisungen bestehen beide die Typprüfung, auch wenn sie fragwürdig sind und in vielen anderen Sprachen Laufzeitfehler produzieren. Dies ist jedoch ein Modell für das Laufzeitverhalten von JavaScript, bei dem 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
       // ~~~~ Operator '+' cannot be applied to types ...
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 property 'toUpperCase' of undefined

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 ausführlicher in Kapitel 5 besprechen werden.

Die Ursache für diese Ausnahmen ist, dass TypeScripts Verständnis vom Typ eines Wertes und die Realität auseinanderklaffen. 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. Wenn dir Solidität wichtig ist, solltest du dir andere Sprachen wie Reason oder Elm ansehen. Diese bieten zwar mehr Garantien für die Sicherheit zur Laufzeit, aber das hat seinen Preis: Beide sind keine Obermenge von JavaScript, sodass die Migration komplizierter wird.

Dinge zum Erinnern

  • TypeScript ist ein Superset von JavaScript. Mit anderen Worten: Alle JavaScript-Programme sind bereits TypeScript-Programme. TypeScript hat eine eigene Syntax, sodass TypeScript-Programme im Allgemeinen keine gültigen JavaScript-Programme sind.

  • TypeScript fügt ein Typsystem hinzu, das das Laufzeitverhalten von JavaScript modelliert und versucht, Code zu erkennen, der zur Laufzeit Ausnahmen auslösen wird. Du solltest aber nicht erwarten, dass es jede Ausnahme erkennt. Es ist möglich, dass Code die Typprüfung besteht, aber trotzdem zur Laufzeit eine Ausnahme auslöst.

  • Das Typsystem von TypeScript ist weitgehend dem Verhalten von JavaScript nachempfunden, aber es gibt einige Konstrukte, die JavaScript zulässt, TypeScript aber ausschließt, z. B. den Aufruf von Funktionen mit der falschen Anzahl von Argumenten. Das ist vor allem eine Frage des Geschmacks.

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, fast 100 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 einsetzen willst. Du kannst sie erstellen, indem du tsc --init aufrufst.

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 steuert, ob Variablen bekannte Typen haben müssen. 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 Typüberprü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 3.

Diese werden implizite any genannt, weil du nie das Wort "any" geschrieben hast und 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). noImplicitAny auszuschalten ist nur sinnvoll, wenn du ein Projekt von JavaScript auf TypeScript umstellst (siehe Kapitel 8).

strictNullChecks steuert, 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 el = document.getElementById('status');
   el.textContent = 'Ready';
// ~~ Object is possibly 'null'

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

strictNullChecks ist sehr hilfreich, wenn es darum geht, Fehler bei den Werten null und undefined zu finden, aber es erhöht die Schwierigkeit, die Sprache zu benutzen. Wenn du ein neues Projekt beginnst, solltest du strictNullChecks einstellen. Wenn du aber neu in der Sprache bist oder eine JavaScript-Codebasis migrierst, kannst du es auch weglassen. Du solltest auf jeden Fall noImplicitAny setzen, bevor du strictNullChecks 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.

Es gibt viele andere Einstellungen, die sich auf die Sprachsemantik auswirken (z.B. noImplicitThis und strictFunctionTypes), aber diese sind im Vergleich zu noImplicitAny und strictNullChecks unwichtig. Um alle diese Prüfungen zu aktivieren, schalte die Einstellung strict ein. TypeScript kann die meisten Fehler mit strict abfangen, also ist dies die Einstellung, bei der du letztendlich landen willst.

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 statt mit Kommandozeilenoptionen.

  • Aktiviere noImplicitAny, es sei denn, du stellst ein JavaScript-Projekt auf TypeScript 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 hohem Niveau 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 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.

Code mit Tippfehlern kann Ausgaben erzeugen

Da die Ausgabe von unabhängig von der Typprü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: Sie deuten wahrscheinlich auf ein Problem hin und sind es wert, untersucht zu werden, aber sie werden den Build nicht stoppen.

Code-Emissionen bei Vorhandensein 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 willst, kannst du die Option noEmitOnError in der tsconfig.json oder die entsprechende Option in deinem Build-Tool verwenden.

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.width * shape.height;
                    //         ~~~~~~ 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.

Um herauszufinden, mit welcher Art von Form du es zu tun hast, brauchst du eine Möglichkeit, ihren Typ zur Laufzeit zu rekonstruieren. In diesem Fall kannst du auf das Vorhandensein einer height Eigenschaft prüfen:

function calculateArea(shape: Shape) {
  if ('height' in shape) {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;
  }
}

Das funktioniert, weil die Eigenschaftsprüfung nur Werte umfasst, die zur Laufzeit verfügbar sind, aber es dem Typprüfer trotzdem ermöglicht, den Typ von shapeauf Rectangle zu verfeinern.

Eine andere Möglichkeit wäre gewesen, 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') {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;
  }
}

Der Typ Shape ist ein Beispiel für eine "tagged union". Weil sie es so einfach machen, Typinformationen zur Laufzeit wiederherzustellen, sind "tagged 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 gewesen, die Klassen Square und Rectangle zu bilden:

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

function calculateArea(shape: Shape) {
  if (shape instanceof Rectangle) {
    shape;  // Type is Rectangle
    return shape.width * shape.height;
  } else {
    shape;  // Type is Square
    return shape.width * shape.width;  // OK
  }
}

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. Diese Unterscheidung ist wichtig zu verstehen, kann aber ziemlich subtil sein. Siehe Punkt 8.

Typoperationen können keine Laufzeitwerte beeinflussen

Angenommen, du hast einen Wert, der eine Zeichenkette oder eine Zahl sein kann, und du möchtest ihn normalisieren, damit er immer eine Zahl ist. Hier ist ein fehlgeleiteter Versuch, der von der Typüberprüfung akzeptiert wird:

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 typeof(val) === 'string' ? Number(val) : val;
}

(as number ist eine Typ-Assertion. Mehr darüber, wann es sinnvoll ist, diese zu verwenden, findest du unter Punkt 9.)

Laufzeittypen sind möglicherweise nicht dieselben wie deklarierte Typen

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?

Der Schlüssel zu ist, sich daran zu erinnern, dass boolean der deklarierte Typ ist. Da es ein TypeScript-Typ ist, verschwindet er zur Laufzeit. Im JavaScript-Code könnte ein Benutzer versehentlich setLightSwitch mit einem Wert wie "ON" aufrufen. Auch in reinem TypeScript gibt es Möglichkeiten, diesen Codepfad 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. Das ist eine Situation, die du nach Möglichkeit vermeiden solltest. Aber sei dir bewusst, dass ein Wert auch andere Typen haben kann als die, die du deklariert hast.

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 Deklarationen 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, b) {
  return a + b;
}

const three = add(1, 2);  // Type is number
const twelve = add('1', '2');  // Type is string

Die ersten beiden Deklarationen von add liefern nur Typinformationen. Wenn TypeScript die JavaScript-Ausgabe erzeugt, werden sie entfernt und nur die Implementierung bleibt übrig. (Wenn du diese Art der Überladung verwendest, sieh dir zuerst Punkt 50 an. Es gibt einige Feinheiten, die du beachten musst.)

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 eine "Transpile only"-Option anbieten, um die Typüberprüfung zu überspringen.

  • Der Code, den TypeScript ausgibt, um ältere Laufzeiten zu unterstützen , kann im Vergleich zu nativen Implementierungen einen Performance-Overhead verursachen. Wenn du z. B. Generatorfunktionen verwendest und auf ES5 abzielt, das vor den Generatoren liegt, dann gibt tsc Hilfscode aus, damit alles funktioniert. Das kann im Vergleich zu einer nativen Implementierung von Generatoren einen gewissen Overhead bedeuten. In jedem Fall hat dies mit dem Emit-Ziel und der Sprache zu tun und ist unabhängig von den Typen.

Dinge zum Erinnern

  • Die Codegenerierung ist unabhängig vom Typsystem. Das bedeutet, dass TypeScript-Typen keinen Einfluss auf das Laufzeitverhalten oder die Leistung 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 class, führen sowohl einen TypeScript-Typ als auch einen zur Laufzeit verfügbaren Wert ein.

Punkt 4: Mach dich mit der Strukturtypisierung vertraut

JavaScript ist von Natur aus duck typed: Wenn du einer Funktion einen Wert mit den richtigen Eigenschaften übergibst, ist es ihr egal, wie du den Wert erzeugt hast. Sie wird ihn einfach verwenden. ("If it walks like a duck and talks like a duck...") TypeScript modelliert dieses Verhalten und 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 * v.x + v.y * v.y);
}

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: 'Zee' };
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. Das Typsystem von TypeScript modelliert das Laufzeitverhalten von JavaScript(Punkt 1). Es erlaubt calculateLength, mit NamedVector aufgerufen zu werden, weil seine Struktur mit Vector2D kompatibel ist. 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, bekommst du wahrscheinlich etwas, das länger ist als die Einheitslänge:

> normalize({x: 3, y: 4, z: 5})
{ x: 0.6, y: 0.8, z: 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. Wir werden auf dieses Beispiel in Punkt 37 zurückkommen.)

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. Dies wird als "versiegelter" oder "präziser" Typ bezeichnet und kann im Typsystem von TypeScript nicht ausgedrückt werden. Ob es dir gefällt oder nicht, deine Typen sind "offen".

Diese 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. Das Iterieren über Objekte kann schwierig sein, wenn es um die korrekte Eingabe geht. Wir werden in Punkt 54 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 auf Zuordenbarkeit verglichen werden:

class C {
  foo: string;
  constructor(foo: string) {
    this.foo = foo;
  }
}

const c = new C('instance of C');
const d: C = { foo: 'object literal' };  // OK!

Warum ist d der C zuweisbar? Sie hat eine foo Eigenschaft, die eine string ist. Außerdem hat sie eine constructor (von Object.prototype), die mit einem Argument aufgerufen werden kann (obwohl sie normalerweise mit Null aufgerufen wird). Die Strukturen stimmen also überein. Diese kann zu Überraschungen führen, wenn du im Konstruktor von Ceine Logik hast und eine Funktion schreibst, die davon ausgeht, dass sie ausgeführt wird. Das ist ein großer Unterschied zu Sprachen wie C++ oder Java, wo die Deklaration eines Parameters vom Typ C garantiert, dass er entweder C oder eine Unterklasse davon ist.

Strukturelle Typisierung ist von Vorteil, wenn du Tests schreibst. Angenommen, du hast eine Funktion, die eine Abfrage in einer Datenbank durchfü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. Ein besserer Ansatz 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 51.

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 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, und optional, weil du die Typüberprüfung jederzeit deaktivieren kannst. Der Schlüssel zu diesen Funktionen ist der Typ any:

   let age: number;
   age = '12';
// ~~~ Type '"12"' is not assignable to type 'number'
   age = '12' as any;  // OK

Der Typprüfer hat Recht, wenn er sich hier beschwert, aber du kannst ihn zum Schweigen bringen, indem du einfach 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 any viele der Vorteile von TypeScript zunichte macht. Du solltest zumindest die Gefahren verstehen, bevor du es benutzt.

Es gibt keine Typensicherheit bei allen Typen

Im vorangegangenen Beispiel sagt die Typdeklaration, dass age ein number ist. Aber any lässt dich ein string zuweisen. Der Typprüfer wird glauben, dass es sich um ein number handelt (das hast du ja schließlich gesagt), und das Chaos bleibt unentdeckt:

age += 1;  // OK; at runtime, age 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 ein Symbol einen Typ hat, können die TypeScript-Sprachdienste eine intelligente Autovervollständigung und kontextbezogene Dokumentation bereitstellen (wie in Abbildung 1-3 gezeigt).

efts 01in01
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).

efts 01in02
Abbildung 1-4. Es gibt keine Autovervollständigung für Eigenschaften von Symbolen mit beliebigen Typen.

Auch das Umbenennen ist ein solcher Dienst. Wenn du einen Personentyp 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).

efts 01in03
Abbildung 1-5. Umbenennen eines Symbols in vscode.
efts 01in04
Abbildung 1-6. Auswählen des neuen Namens. Der TypeScript-Sprachdienst sorgt dafür, dass alle Verwendungen des Symbols im Projekt ebenfalls umbenannt werden.

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}`;

Das Motto von TypeScript ist "JavaScript, das skaliert". Ein wichtiger Bestandteil von "skaliert" sind die Sprachdienste, die ein zentraler 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 Fehlern 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 ein Item 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, also ist es 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 deinen Anwendungsstatus kann ziemlich lang werden. Anstatt Typen für die Dutzenden von Eigenschaften im Status deiner Seite 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 problematisch, weil es das Design deines Zustands 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 entdeckt, stärkt das dein Vertrauen in das Typsystem. Aber wenn du zur Laufzeit einen Typfehler siehst, 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. Mehr darüber, wie du die Nachteile von any einschränken kannst, erfährst du in Kapitel 5.

Dinge zum Erinnern

  • Der Typ any bringt den Type-Checker und die TypeScript-Sprachdienste zum Schweigen. Er kann echte Probleme verschleiern, die Erfahrung der Entwickler beeinträchtigen und das Vertrauen in das Typsystem untergraben. Vermeide seine Verwendung, wenn du kannst!

Get Effektives TypeScript 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.