Kapitel 1. Projekt einrichten

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

Du willst mit TypeScript loslegen, fantastisch! Die große Frage ist: Wie fängst du an? Es gibt viele Möglichkeiten, TypeScript in deine Projekte zu integrieren, und alle unterscheiden sich je nach den Anforderungen deines Projekts. Genauso wie JavaScript auf vielen Runtimes läuft, gibt es viele Möglichkeiten, TypeScript so zu konfigurieren, dass es den Anforderungen deines Ziels entspricht.

In diesem Kapitel geht es um alle Möglichkeiten, TypeScript in dein Projekt einzubinden: als Erweiterung neben JavaScript, die dir grundlegende Autovervollständigung und Fehleranzeige bietet, bis hin zu vollwertigen Setups für Full-Stack-Anwendungen auf Node.js und im Browser.

Da das JavaScript-Tooling ein Feld mit unendlichen Möglichkeiten ist - manche sagen, dass jede Woche eine neue JavaScript-Build-Chain veröffentlicht wird, fast so viel wie neue Frameworks - konzentriert sich dieses Kapitel mehr darauf, was du mit dem TypeScript-Compiler allein, also ohne ein zusätzliches Tool, tun kannst.

TypeScript bietet alles, was du für deine Transpilierungsanforderungen brauchst, außer der Möglichkeit, minifizierte und optimierte Bundles für die Webverteilung zu erstellen. Bundler wie ESBuild oder Webpack kümmern sich um diese Aufgabe. Außerdem gibt es Setups, die andere Transpiler wie Babel.js enthalten, die gut mit TypeScript zusammenarbeiten können.

Bundler und andere Transpiler sind nicht Gegenstand dieses Kapitels. Lies in deren Dokumentation nach, wie TypeScript eingebunden wird, und nutze das Wissen in diesem Kapitel, um die richtige Konfiguration zu erstellen.

Da TypeScript ein Projekt mit mehr als einem Jahrzehnt Geschichte ist, gibt es einige Überbleibsel aus älteren Zeiten, die TypeScript aus Kompatibilitätsgründen nicht einfach loswerden kann. Deshalb werden in diesem Kapitel die moderne JavaScript-Syntax und die jüngsten Entwicklungen bei den Webstandards beleuchtet.

Wenn du immer noch auf Internet Explorer 8 oder Node.js 10 abzielen musst, dann tut es mir erstens leid, denn für diese Plattformen ist es wirklich schwer zu entwickeln. Aber zweitens: Mit dem Wissen aus diesem Kapitel und der offiziellen TypeScript-Dokumentation kannst du die Teile für ältere Plattformen zusammensetzen.

1.1 Typ-Prüfung von JavaScript

Problem

Du willst mit möglichst wenig Aufwand eine grundlegende Typüberprüfung für JavaScript erreichen.

Lösung

Füge einen einzeiligen Kommentar mit @ts-check am Anfang jeder JavaScript-Datei ein, die du auf ihre Typisierung überprüfen willst. Mit den richtigen Editoren erhältst du bereits rote, verschnörkelte Linien, wenn TypeScript auf Dinge stößt, die nicht ganz passen.

Diskussion

TypeScript wurde als Obermenge von JavaScript entwickelt, und jedes gültige JavaScript ist auch gültiges TypeScript. Das bedeutet, dass TypeScript auch sehr gut darin ist, potenzielle Fehler in regulärem JavaScript-Code zu erkennen.

Wir können diese Funktion nutzen, wenn wir kein vollständiges TypeScript-Setup wollen, sondern einige grundlegende Hinweise und Typüberprüfungen, um unseren Entwicklungsworkflow zu erleichtern.

Eine gute Voraussetzung, wenn du nur JavaScript typisieren willst, ist ein guter Editor oder eine gute IDE. Ein Editor, der sehr gut mit TypeScript zusammenarbeitet, ist Visual Studio Code. Visual Studio Code - oder kurz VSCode - war das erste große Projekt, das TypeScript verwendet hat, noch vor der Veröffentlichung von TypeScript.

Viele Leute empfehlen VSCode, wenn du JavaScript oder TypeScript schreiben willst. Aber eigentlich ist jeder Editor gut, solange er TypeScript unterstützt. Und das tun heutzutage die meisten von ihnen.

Mit Visual Studio Code erhalten wir eine sehr wichtige Funktion für die Typüberprüfung von JavaScript: rote, verschnörkelte Linien, wenn etwas nicht ganz stimmt, wie du in Abbildung 1-1 sehen kannst. Dies ist die niedrigste Einstiegshürde. Das Typsystem von TypeScript hat verschiedene Stufen der Strenge, wenn du mit einer Codebasis arbeitest.

tscb 0101
Abbildung 1-1. Rote, verschnörkelte Linien in Code-Editoren: ein erstes Feedback, wenn etwas in deinem Code nicht stimmt

Zunächst versucht das Typsystem, aus der Verwendung von JavaScript-Code Typen abzuleiten. Wenn du eine Zeile wie diese in deinem Code hast:

let a_number = 1000;

TypeScript wird number korrekt als Typ von a_number ableiten.

Eine Schwierigkeit bei JavaScript ist, dass die Typen dynamisch sind. Bindungen über let, var oder const können den Typ je nach Verwendung ändern.1 Sieh dir das folgende Beispiel an:

let a_number = 1000;

if (Math.random() < 0.5) {
  a_number = "Hello, World!";
}

console.log(a_number * 10);

Wir weisen a_number eine Zahl zu und ändern die Bindung in string, wenn die Bedingung in der nächsten Zeile als wahr ausgewertet wird. Das wäre kein großes Problem, wenn wir nicht versuchen würden, a_number in der letzten Zeile zu multiplizieren. In etwa 50 % aller Fälle führt dieses Beispiel zu unerwünschtem Verhalten.

TypeScript kann hier helfen. Durch das Hinzufügen eines einzeiligen Kommentars mit @ts-check ganz oben in unserer JavaScript-Datei aktiviert TypeScript die nächste Stufe der Strenge: Die Typüberprüfung von JavaScript-Dateien basiert auf den in derJavaScript-Datei verfügbaren Typinformationen.

In unserem Beispiel wird TypeScript herausfinden, dass wir versucht haben, eine Zeichenkette einer Bindung zuzuweisen, die TypeScript für eine Zahl gehalten hat. Wir erhalten eine Fehlermeldung in unserem Editor:

// @ts-check
let a_number = 1000;

if (Math.random() < 0.5) {
  a_number = "Hello, World!";
// ^-- Type 'string' is not assignable to type 'number'.ts(2322)
}

console.log(a_number * 10);

Jetzt können wir anfangen, unseren Code zu korrigieren, und TypeScript wird uns dabei helfen.

Die Typinferenz für JavaScript ist sehr weitreichend. Im folgenden Beispiel schlussfolgert TypeScript Typen anhand von Operationen wie Multiplikation und Addition sowie von Standardwerten:

function addVAT(price, vat = 0.2) {
  return price * (1 + vat);
}

Die Funktion addVat benötigt zwei Argumente. Das zweite Argument ist optional, da es auf den Standardwert 0.2 gesetzt ist. TypeScript wird dich warnen, wenn du versuchst, einen Wert zu übergeben, der nicht funktioniert:

addVAT(1000, "a string");
//           ^-- Argument of type 'string' is not assignable
//               to parameter of type 'number'.ts(2345)

Da wir Multiplikations- und Additionsoperationen innerhalb des Funktionskörpers verwenden, versteht TypeScript, dass wir eine Zahl aus dieser Funktion zurückgeben werden:

addVAT(1000).toUpperCase();
//           ^-- Property 'toUpperCase' does not
//               exist on type 'number'.ts(2339)

In manchen Situationen brauchst du mehr als eine Typinferenz. In JavaScript-Dateien kannst du Funktionsargumente und Bindungen mit JSDoc-Typ-Annotationen versehen. JSDoc ist eine Kommentar-Konvention, mit der du deine Variablen undFunktionsschnittstellen so beschreiben kannst, dass sie nicht nur für Menschen lesbar, sondern auch für Maschinen interpretierbar sind. TypeScript nimmt deine Annotationen auf und verwendet sie als Typen für das Typsystem:

/** @type {number} */
let amount;

amount = '12';
//       ^-- Argument of type 'string' is not assignable
//           to parameter of type 'number'.ts(2345)

/**
 * Adds VAT to a price
 *
 * @param {number} price The price without VAT
 * @param {number} vat The VAT [0-1]
 *
 * @returns {number}
 */
function addVAT(price, vat = 0.2) {
  return price * (1 + vat);
}

JSDoc ermöglicht es dir auch, neue, komplexe Typen für Objekte zu definieren:

/**
 * @typedef {Object} Article
 * @property {number} price
 * @property {number} vat
 * @property {string} string
 * @property {boolean=} sold
 */

/**
 * Now we can use Article as a proper type
 * @param {[Article]} articles
 */
function totalAmount(articles) {
  return articles.reduce((total, article) => {
    return total + addVAT(article);
  }, 0);
}

Die Syntax könnte sich allerdings etwas klobig anfühlen; wir werden in Rezept 1.3 bessere Möglichkeiten finden, Objekte zu annotieren.

Wenn du eine JavaScript-Codebasis hast, die mit JSDoc gut dokumentiert ist, kannst du durch das Hinzufügen einer einzigen Zeile zu deinen Dateien sehr gut verstehen, wenn in deinem Code etwas schief läuft.

1.2 Installation von TypeScript

Problem

Rote Schnörkel im Editor reichen nicht aus: Du brauchst Kommandozeilen-Feedback, Statuscodes, Konfigurationsmöglichkeiten und Optionen für die Typüberprüfung von JavaScript und die Kompilierung von TypeScript.

Lösung

Installiere TypeScript über die primäre Paketregistrierung von Node: NPM.

Diskussion

TypeScript ist in TypeScript geschrieben, zu JavaScript kompiliert und nutzt die Node.js JavaScript-Laufzeitumgebung als primäre Ausführungsumgebung.2 Auch wenn du keine Node.js-Applikation schreibst, läuft das Tooling für deine JavaScript-Anwendungen auf Node. Besorge dir also Node.js von der offiziellen Website und mache dich mit den Kommandozeilen-Tools vertraut.

Bei einem neuen Projekt musst du sicherstellen, dass du den Projektordner mit einer neuenpackage.json initialisierst. Diese Datei enthält alle Informationen, die Node und sein Paketmanager NPM benötigen, um den Inhalt deines Projekts zu erkennen. Erstelle mit dem NPM-Kommandozeilen-Tool eine neue package.json-Datei mit den Standardinhalten im Ordner deines Projekts:

$ npm init -y
Hinweis

In diesem Buch wirst du immer wieder Befehle sehen, die in deinem Terminal ausgeführt werden sollten. Der Einfachheit halber zeigen wir diese Befehle so, wie sie in der BASH oder ähnlichen Shells für Linux, macOS oder dem Windows-Subsystem für Linux erscheinen würden. Das führende $ Zeichen ist eine Konvention, um einen Befehl zu kennzeichnen, aber es ist nicht dazu gedacht, von dir geschrieben zu werden. Beachte, dass alle Befehle auch auf der regulärenWindows-Befehlszeilenoberfläche und in der PowerShell funktionieren.

NPM ist der Paketmanager von Node. Er verfügt über ein CLI, eine Registry und andere Tools, mit denen du Abhängigkeiten installieren kannst. Sobald du deine package.json initialisiert hast, installierst duTypeScript mit NPM. Wir installieren es als Entwicklungsabhängigkeit, was bedeutet, dass TypeScript nicht enthalten ist, wenn du dein Projekt als Bibliothek beiNPM veröffentlichen willst:

$ npm install -D typescript

Du kannst TypeScript global installieren, damit du den TypeScript-Compiler überall zur Verfügung hast, aber ich empfehle dringend, TypeScript für jedes Projekt separat zu installieren. Je nachdem, wie häufig du deine Projekte besuchst, wirst du unterschiedliche TypeScript-Versionen haben, die mit dem Code deines Projekts synchronisiert sind. Wenn du TypeScript global installierst (und aktualisierst), kann es sein, dass Projekte, die du eine Weile nicht angefasst hast, kaputt gehen.

Hinweis

Wenn du Frontend-Abhängigkeiten über NPM installierst, brauchst du ein zusätzliches Tool, um sicherzustellen, dass dein Code auch in deinem Browser läuft: einen Bundler. Da TypeScript keinen Bundler enthält, der mit den unterstützten Modulsystemen funktioniert, musst du die richtigen Tools einrichten. Tools wie Webpack sind weit verbreitet, ebenso wie ESBuild. Alle Tools sind so konzipiert, dass sie auch TypeScript ausführen können. Du kannst aber auch komplett nativ arbeiten, wie in Rezept 1.8 beschrieben.

Jetzt, wo TypeScript installiert ist, initialisiere ein neues TypeScript-Projekt. Verwende dazu NPX: Damit kannst du ein Kommandozeilenprogramm ausführen, das du im Zusammenhang mit deinem Projekt installiert hast.

Mit:

$ npx tsc --init

kannst du die lokale Version des TypeScript-Compilers für dein Projekt ausführen und das Flag init übergeben, um eine neue tsconfig.json zu erstellen.

Die tsconfig.json ist die wichtigste Konfigurationsdatei für dein TypeScript-Projekt. Sie enthält alle notwendigen Konfigurationen, damit TypeScript weiß, wie dein Code zu interpretieren ist, wie Typen für Abhängigkeiten verfügbar gemacht werden und ob du bestimmte Funktionen ein- oder ausschalten musst.

Standardmäßig setzt TypeScript diese Optionen für dich:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Schauen wir sie uns im Detail an.

target ist es2016, was bedeutet, dass der TypeScript-Compiler deine TypeScript-Dateien mit einer ECMAScript 2016-kompatiblen Syntax kompiliert, wenn du ihn ausführst. Je nach den von dir unterstützten Browsern oder Umgebungen kannst du entweder eine neuere Version wählen (ECMAScript-Versionen werden nach dem Jahr der Veröffentlichung benannt) oder eine ältere, z. B. es5 für Leute, die sehr alte Internet Explorer-Versionen unterstützen müssen. Ich hoffe natürlich, dass du das nicht musst.

module ist commonjs. Damit kannst du ECMAScript-Modulsyntax schreiben, aber anstatt diese Syntax in die Ausgabe zu übertragen, kompiliert TypeScript sie in das CommonJS-Format. Das bedeutet Folgendes:

import { name } from "./my-module";

console.log(name);
//...

wird:

const my_module_1 = require("./my-module");
console.log(my_module_1.name);

sobald du kompiliert hast. CommonJS war das Modulsystem für Node.js und ist aufgrund der Popularität von Node sehr weit verbreitet. Node.js hat inzwischen auch ECMAScript-Module übernommen, mit denen wir uns in Rezept 1.9 beschäftigen werden.

esModuleInterop stellt sicher, dass Module, die keine ECMAScript-Module sind, nach dem Import an den Standard angepasst werden. hilft Leuten, die case-sensitive Dateisysteme verwenden, mit Leuten zusammenzuarbeiten, die case-insensitive Dateisysteme verwenden. Und geht davon aus, dass deine installierten Typdefinitionsdateien (mehr dazu später) keine Fehler enthalten. Dein Compiler wird sie also nicht überprüfen und etwas schneller werden. forceConsistentCasingInFileNames skipLibCheck

Eine der interessantesten Funktionen ist der Strict-Modus von TypeScript. Wenn er auf true eingestellt ist, verhält sich TypeScript in bestimmten Bereichen anders. Auf diese Weise kann das TypeScript-Team festlegen, wie sich das Typsystem verhalten soll.

Wenn TypeScript eine bahnbrechende Änderung einführt, weil sich die Sichtweise auf das Typsystem ändert, wird sie in den Strict-Modus übernommen. Das bedeutet letztlich, dass dein Code kaputt gehen kann, wenn du TypeScript aktualisierst und immer im Strict-Modus ausführst.

Damit du Zeit hast, dich auf die Änderungen einzustellen, kannst du in TypeScript auch bestimmte Funktionen des Strict Mode ein- oder ausschalten, Funktion für Funktion.

Zusätzlich zu den Standardeinstellungen empfehle ich dringend zwei weitere:

{
  "compilerOptions": {
    //...
    "rootDir": "./src",
    "outDir": "./dist"
  }
}

Damit wird TypeScript angewiesen, die Quelldateien aus einem src-Ordner zu holen und die kompilierten Dateien in einem dist-Ordner abzulegen. Auf diese Weise kannst du die kompilierten Dateien von denen trennen, die du schreibst. Den src-Ordner musst du natürlich erstellen; der dist-Ordner wird nach dem Kompilieren erstellt.

Oh, Kompilierung. Sobald du dein Projekt eingerichtet hast, erstelle eine index.ts-Datei in src:

console.log("Hello World");

Die Erweiterung .ts zeigt an, dass es sich um eine TypeScript-Datei handelt. Führe sie jetzt aus:

$ npx tsc

in deiner Befehlszeile ein und sieh dem Compiler bei der Arbeit zu.

1.3 Typen auf der Seite halten

Problem

Du willst normales JavaScript schreiben, ohne einen zusätzlichen Build-Schritt zu machen, aber trotzdem Editor-Unterstützung und richtige Typinformationen für deine Funktionen erhalten. Du willst aber nicht deine komplexen Objekttypen mit JSDoc definieren, wie in Rezept 1.1 gezeigt.

Lösung

Behalte Typdefinitionsdateien "auf der Seite" und führe den TypeScript-Compiler im Modus "JavaScript prüfen" aus.

Diskussion

Die schrittweise Einführung war schon immer ein wichtiges Ziel für TypeScript. Mit dieser Technik, die ich "types on the side" genannt habe, kannst du TypeScript-Syntax für Objekttypen und fortgeschrittene Funktionen wie Generics und bedingte Typen (siehe Kapitel 5) anstelle von klobigen JSDoc-Kommentaren schreiben, aber du schreibst weiterhin JavaScript für deine eigentliche Anwendung.

Irgendwo in deinem Projekt, vielleicht in einem @types-Ordner, legst du eine Typdefinitionsdatei an. Sie hat die Endung .d.ts und dient im Gegensatz zu normalen .ts-Dateien dazu, Deklarationen, aber keinen eigentlichen Code zu enthalten.

Hier kannst du deine Schnittstellen, Typ-Aliase und komplexen Typen schreiben:

// @types/person.d.ts

// An interface for objects of this shape
export interface Person {
  name: string;
  age: number;
}

// An interface that extends the original one
// this is tough to write with JSDoc comments alone.
export interface Student extends Person {
  semester: number;
}

Beachte, dass du die Schnittstellen aus den Deklarationsdateien exportierst. So kannst du sie in deine JavaScript-Dateien importieren:

// index.js
/** @typedef { import ("../@types/person").Person } Person */

Der Kommentar in der ersten Zeile weist TypeScript an, den Typ Person aus @types/person zu importieren und ihn unter dem Namen Person verfügbar zu machen.

Jetzt kannst du diesen Bezeichner verwenden, um Funktionsparameter oder Objekte zu annotieren, genauso wie du es mit primitiven Typen wie string tun würdest:

// index.js, continued

/**
 * @param {Person} person
 */
function printPerson(person) {
  console.log(person.name);
}

Um sicherzustellen, dass du eine Rückmeldung vom Editor erhältst, musst du wie in Rezept 1.1 beschrieben @ts-check an den Anfang deiner JavaScript-Dateien setzen. Oder du kannst dein Projekt so konfigurieren, dass JavaScript immer überprüft wird.

Öffne tsconfig.json und setze das checkJs Flag auf true. Dadurch werden alle JavaScript-Dateien aus deinem src-Ordner abgeholt und du bekommst ständig Rückmeldungen über Tippfehler in deinem Editor. Du kannst auch npx tsc ausführen, um zu sehen, ob du Fehler in deiner Kommandozeile hast.

Wenn du nicht möchtest, dass TypeScript deine JavaScript-Dateien in ältere Versionen von JavaScript transpiliert, stelle sicher, dass du noEmit auf true setzt:

{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
  }
}

Damit schaut sich TypeScript deine Quelldateien an und gibt dir alle Typinformationen, die du brauchst, aber es berührt deinen Code nicht.

Diese Technik ist auch dafür bekannt, dass sie skalierbar ist. Bekannte JavaScript-Bibliotheken wie Preact arbeiten auf diese Weise und bieten sowohl ihren Nutzern als auch ihren Mitwirkenden fantastische Werkzeuge.

1.4 Ein Projekt nach TypeScript migrieren

Problem

Du möchtest die Vorteile von TypeScript für dein Projekt nutzen, musst aber eine ganze Codebasis migrieren.

Lösung

Benenne deine Module Datei für Datei von .js in .ts um. Nutze verschiedene Compiler-Optionen und Funktionen, die dir helfen, Fehler auszubügeln.

Diskussion

Der Vorteil von TypeScript-Dateien anstelle von JavaScript-Dateien mit Typen ist, dass sich deine Typen und Implementierungen in einer Datei befinden, was dir bessere Editorunterstützung und Zugang zu mehr TypeScript-Funktionen bietet und die Kompatibilität mit anderen Tools erhöht.

Wenn du jedoch einfach alle Dateien von .js in .ts umbenennst, wird das höchstwahrscheinlich zu einer Menge Fehler führen. Deshalb solltest du eine Datei nach der anderen umbenennen und die Typensicherheit nach und nach erhöhen.

Das größte Problem bei der Migration ist, dass du es plötzlich mit einem TypeScript-Projekt zu tun hast, nicht mit JavaScript. Dennoch werden viele deiner Module aus JavaScript bestehen, und ohne Typinformationen werden sie bei der Typüberprüfung fehlschlagen.

Mach es dir und TypeScript leichter, indem du die Typüberprüfung für JavaScript ausschaltest, aber erlaubst, dass TypeScript-Module JavaScript-Dateien laden und auf sie verweisen:

{
  "compilerOptions": {
    "checkJs": false,
    "allowJs": true
  }
}

Wenn du jetzt npx tsc ausführst, wirst du sehen, dass TypeScript alle JavaScript- und TypeScript-Dateien in deinem Quellordner aufnimmt und entsprechende JavaScript-Dateien in deinem Zielordner erstellt. Außerdem transpiliert TypeScript deinen Code, damit er mit der angegebenen Zielversion kompatibel ist.

Wenn du mit Abhängigkeiten arbeitest, wirst du feststellen, dass einige von ihnen keine Typinformationen enthalten. Das führt auch zu TypeScript-Fehlern:

import _ from "lodash";
//            ^- Could not find a declaration
//               file for module 'lodash'.

Installiere Typdefinitionen von Drittanbietern, um diesen Fehler zu beheben. Siehe Rezept 1.5.

Wenn du eine Datei nach der anderen migrierst, wirst du feststellen, dass du nicht alle Typisierungen für eine Datei auf einmal bekommen kannst. Es gibt Abhängigkeiten und du wirst schnell in den Kaninchenbau geraten, weil du zu viele Dateien anpassen musst, bevor du diejenige in Angriff nehmen kannst, die du tatsächlich brauchst.

Du kannst jederzeit entscheiden, ob du mit dem Fehler leben willst. Standardmäßig setzt TypeScript die Compiler-Option noEmitOnError auf false:

{
  "compilerOptions": {
    "noEmitOnError": false
  }
}

Das bedeutet, dass TypeScript unabhängig davon, wie viele Fehler du in deinem Projekt hast, Ergebnisdateien erzeugt und versucht, dich nicht zu blockieren. Diese Einstellung solltest du vielleicht nach der Migration aktivieren.

Im Strict-Modus ist das TypeScript-Feature-Flag noImplicitAny auf true gesetzt. Dieses Flag stellt sicher, dass du nicht vergisst, einer Variablen, Konstanten oder einem Funktionsparameter einen Typ zuzuweisen. Auch wenn es nur any ist:

function printPerson(person: any) {
  // This doesn't make sense, but is ok with any
  console.log(person.gobbleydegook);
}

// This also doesn't make sense, but any allows it
printPerson(123);

any ist der Catchall-Typ in TypeScript. Jeder Wert ist mit kompatibel, und ermöglicht dir den Zugriff auf jede Eigenschaft oder den Aufruf jeder Methode. schaltet die Typüberprüfung effektiv aus, sodass du während deines Migrationsprozesses etwas Luft zum Atmen hast. any any any

Alternativ kannst du deine Parameter mit unknown annotieren. Damit kannst du auch alles an eine Funktion übergeben, aber du kannst nichts damit machen, bis du mehr über den Typ weißt.

Du kannst auch entscheiden, Fehler zu ignorieren, indem du einen @ts-ignore Kommentar vor der Zeile einfügst, die du von der Typprüfung ausschließen möchtest. Ein @ts-nocheck Kommentar am Anfang deiner Datei schaltet die Typenprüfung für dieses bestimmte Modul komplett aus.

Eine Kommentar-Direktive, die sich fantastisch für die Migration eignet, ist @ts-expect-error. Sie funktioniert wie @ts-ignore, denn sie schluckt Fehler aus dem Verlauf der Typüberprüfung, erzeugt aber rote, verschnörkelte Linien, wenn kein Typfehler gefunden wird.

Bei der Migration hilft dir das, die Stellen zu finden, die du erfolgreich auf TypeScript umgestellt hast. Wenn es keine @ts-expect-error Direktiven mehr gibt, bist du fertig:

function printPerson(person: Person) {
  console.log(person.name);
}

// This error will be swallowed
// @ts-expect-error
printPerson(123);


function printNumber(nr: number) {
  console.log(nr);
}

// v- Unused '@ts-expect-error' directive.ts(2578)
// @ts-expect-error
printNumber(123);

Das Tolle an dieser Technik ist, dass du die Verantwortlichkeiten umkehrst. Normalerweise musst du sicherstellen, dass du die richtigen Werte an eine Funktion übergibst; jetzt kannst du sicherstellen, dass die Funktion in der Lage ist, die richtige Eingabe zu verarbeiten.

Alle Möglichkeiten, Fehler während des Migrationsprozesses loszuwerden, haben eines gemeinsam: Sie sind explizit. Du musst @ts-expect-error Kommentare explizit setzen, Funktionsparameter als any kennzeichnen oder Dateien ganz von der Typüberprüfung ausschließen. Auf diese Weise kannst du während des Migrationsprozesses immer wieder nach diesen Fluchtluken suchen und sicherstellen, dass du sie im Laufe der Zeit alle loswirst.

1.5 Laden von Typen aus Definitely Typed

Problem

Du verlässt dich auf eine Abhängigkeit, die nicht in TypeScript geschrieben wurde und daher keine Typisierung aufweist.

Lösung

Installiere die von der Gemeinschaft gepflegten Typdefinitionen von Definitely Typed.

Diskussion

Definitely Typed ist eines der größten und aktivsten Repositories auf GitHub und sammelt hochwertige TypeScript-Typdefinitionen, die von derCommunity entwickelt und gepflegt werden.

Die Zahl der gepflegten Typdefinitionen liegt bei fast 10.000, und es gibt kaum eine JavaScript-Bibliothek, die nicht verfügbar ist.

Alle Typdefinitionen werden in der Node.js-Paketregistrierung NPM unter dem Namespace @types gelintet, geprüft und bereitgestellt. NPM hat einen Indikator auf der Informationsseite jedes Pakets, der anzeigt, ob Definitely Typed Typdefinitionen verfügbar sind, wie du in Abbildung 1-2 sehen kannst.

tscb 0102
Abbildung 1-2. Die NPM-Website für React zeigt ein DT-Logo neben dem Paketnamen an; diesweist auf verfügbare Typdefinitionen von Definitely Typed

Wenn du auf dieses Logo klickst, gelangst du auf die eigentliche Seite für Typdefinitionen. Wenn für ein Paket bereits Erstanbieter-Typdefinitionen verfügbar sind, wird ein kleines TS-Logo neben dem Paketnamen angezeigt, wie in Abbildung 1-3 dargestellt.

tscb 0103
Abbildung 1-3. Typdefinitionen für React aus Definitely Typed

Um z.B. Typisierungen für das beliebte JavaScript-Framework React zu installieren, installierst du das Paket @types/react zu deinen lokalen Abhängigkeiten:

# Installing React
$ npm install --save react

# Installing Type Definitions
$ npm install --save-dev @types/react
Hinweis

In diesem Beispiel installieren wir Typen in die Entwicklungsabhängigkeiten, da wir sie während der Entwicklung der Anwendung verbrauchen und das kompilierte Ergebnis die Typen ohnehin nicht benötigt.

Standardmäßig greift TypeScript auf Typdefinitionen zurück, die sich in sichtbaren @types-Ordnern relativ zum Stammordner deines Projekts befinden. Außerdem werden alle Typdefinitionen aus node_modules/@types übernommen; beachte, dass NPM dortz. B. @types/react installiert hat.

Wir tun dies, weil die Compiler-Option typeRoots in tsconfig.json auf @types und ./node_modules/@types gesetzt ist. Wenn du diese Einstellung überschreiben willst, musst du die Originalordner mit einbeziehen, wenn du die Typdefinitionen von Definitely Typed übernehmen willst:

{
  "compilerOptions": {
    "typeRoots": ["./typings", "./node_modules/@types"]
  }
}

Wenn du Typdefinitionen in node_modules/@types installierst, lädt TypeScript sie beim Kompilieren. Das heißt, wenn einige Typen Globals deklarieren, werden diese von TypeScript übernommen.

Vielleicht möchtest du explizit angeben, welche Pakete zum globalen Geltungsbereich beitragen dürfen, indem du sie in der Einstellung types in deinen Compileroptionen angibst:

{
  "compilerOptions": {
    "types": ["node", "jest"]
  }
}

Beachte, dass sich diese Einstellung nur auf die Beiträge zum globalen Bereich auswirkt. Wenn du Node-Module über Import-Anweisungen lädst, holt sich TypeScript trotzdem die richtigen Typen aus @types:

// If `@types/lodash` is installed, we get proper
// type defintions for this NPM package
import _ from "lodash"

const result = _.flattenDeep([1, [2, [3, [4]], 5]]);

Wir werden diese Einstellung in Rezept 1.7 wieder aufgreifen.

1.6 Einrichten eines Full-Stack-Projekts

Problem

Du willst eine Full-Stack-Anwendung schreiben, die auf Node.js und den Browser ausgerichtet ist und gemeinsame Abhängigkeiten hat.

Lösung

Erstelle zwei tsconfig-Dateien für jedes Frontend und Backend und lade gemeinsameAbhängigkeiten als Composites.

Diskussion

Node.js und der Browser führen beide JavaScript aus, aber sie haben ein sehr unterschiedliches Verständnis davon, was Entwickler mit der Umgebung machen sollten. Node.js ist für Server, Kommandozeilen-Tools und alles, was ohne eine Benutzeroberfläche läuft, gedacht - ohne Kopf. Es hat eine eigene Reihe von APIs und eine Standardbibliothek. Dieses kleine Skript startet einen HTTP-Server:

const http = require('http'); 1

const hostname = '127.0.0.1';
const port = process.env.PORT || 3000; 2

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`); 3
});

Obwohl es sich zweifellos um JavaScript handelt, sind einige Dinge einzigartig für Node.js:

1

"http" ist ein eingebautes Node.js-Modul für alles, was mit HTTP zu tun hat. Es wird über require geladen, was ein Indikator für das Modulsystem von Node ist, das CommonJS heißt. Es gibt noch andere Möglichkeiten, Module in Node.js zu laden, wie wir in Rezept 1.9 sehen, aber in letzter Zeit ist CommonJS am weitesten verbreitet.

2

Das process Objekt ist ein globales Objekt, das Informationen über Umgebungsvariablen und den aktuellen Node.js-Prozess im Allgemeinen enthält. Auch dies ist einzigartig für Node.js.

3

Die console und ihre Funktionen sind in fast jeder JavaScript-Laufzeitumgebung verfügbar, aber was sie in Node tut, unterscheidet sich von dem, was sie im Browser tut. In Node wird es auf STDOUT ausgegeben, im Browser wird eine Zeile an die Entwicklungswerkzeuge gesendet.

Natürlich gibt es noch viele weitere einzigartige APIs für Node.js. Aber das Gleiche gilt für JavaScript im Browser:

import { msg } from `./msg.js`; 1

document.querySelector('button')?.addEventListener("click", () => { 2
  console.log(msg); 3
});
1

Nachdem es jahrelang keine Möglichkeit gab, Module zu laden, haben ECMAScript-Module ihren Weg in JavaScript und die Browser gefunden. Diese Zeile lädt ein Objekt aus einem anderen JavaScript-Modul. Das läuft nativ im Browser und ist ein zweites Modulsystem für Node.js (siehe Rezept 1.9).

2

JavaScript im Browser ist für die Interaktion mit UI-Ereignissen gedacht. Das document Objekt und die Idee eines querySelector, das auf Elemente im Document Object Model (DOM) verweist, gibt es nur im Browser. Das gilt auch für das Hinzufügen eines Ereignis-Listeners und das Abhören von "Klick"-Ereignissen. Das gibt es in Node.js nicht.

3

Und wieder: console. Es hat die gleiche API wie in Node.js, aber das Ergebnis ist ein bisschenanders.

Die Unterschiede sind so groß, dass es schwierig ist, ein einziges TypeScript-Projekt zu erstellen, das beides abdeckt. Wenn du eine Full-Stack-Anwendung schreibst, musst du zwei TypeScript-Konfigurationsdateien erstellen, die sich mit jedem Teil deines Stacks befassen.

Lass uns zuerst am Backend arbeiten. Nehmen wir an, du willst einen Express.js-Server in Node.js schreiben (Express ist ein beliebtes Server-Framework für Node). Zuerst erstellst du ein neues NPM-Projekt, wie in Rezept 1.1 gezeigt. Dann installierst du Express als Abhängigkeit:

$ npm install --save express

Und installiere Typdefinitionen für Node.js und Express von Definitely Typed:

$ npm install -D @types/express @types/node

Erstelle einen neuen Ordner namens server. Hier wird dein Node.js-Code gespeichert. Anstatt eine neue tsconfig.json über tsc zu erstellen, erstelle eine neue tsconfig.json im Server-Ordner deines Projekts. Hier sind die Inhalte:

// server/tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["ESNext"],
    "module": "commonjs",
    "rootDir": "./",
    "moduleResolution": "node",
    "types": ["node"],
    "outDir": "../dist/server",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Vieles davon solltest du bereits wissen, aber ein paar Dinge fallen auf:

  • Die Eigenschaft module ist auf commonjs gesetzt, das ursprüngliche Node.js-Modulsystem. Alle import und export Anweisungen werden in ihr CommonJS-Pendant transpiliert.

  • Die Eigenschaft types wird auf ["node"] gesetzt. Diese Eigenschaft umfasst alle Bibliotheken, die du global verfügbar haben möchtest. Wenn "node" im globalen Bereich liegt, erhältst du Typinformationen für require, process und andere Node.js-Spezifika, die im globalen Bereich liegen.

Um deinen serverseitigen Code zu kompilieren, führe aus:

$ npx tsc -p server/tsconfig.json

Nun zum Kunden:

// client/tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["DOM", "ESNext"],
    "module": "ESNext",
    "rootDir": "./",
    "moduleResolution": "node",
    "types": [],
    "outDir": "../dist/client",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Es gibt einige Ähnlichkeiten, aber auch hier stechen ein paar Dinge hervor:

  • Du fügst DOM zu der Eigenschaft lib hinzu. Damit erhältst du Typdefinitionen für alles, was mit dem Browser zu tun hat. Wo du Node.js-Typisierungen über Definitely Typed installieren musstest, liefert TypeScript die neuesten Typdefinitionen für den Browser mit dem Compiler.

  • Das Array types ist leer. Damit wird "node" aus unseren globalen Typisierungen entfernt. Da du Typdefinitionen nur pro package.json installieren kannst, wären die "node" Typdefinitionen, die wir zuvor installiert haben, in der gesamten Codebasis verfügbar. Für denclient Teil willst du sie jedoch loswerden.

Um deinen Frontend-Code zu kompilieren, führe aus:

$ npx tsc -p client/tsconfig.json

Bitte beachte, dass du zwei verschiedene tsconfig.json-Dateien konfiguriert hast. Editoren wie Visual Studio Code erfassen Konfigurationsinformationen nur für tsconfig.json-Dateien pro Ordner. Du könntest sie auch tsconfig.server.json und tsconfig.client.json nennen und sie im Stammordner deines Projekts ablegen (und alle Verzeichniseigenschaften anpassen). tsc wird die richtigen Konfigurationen verwenden und Fehler melden, wenn es welche findet, aber der Editor wird meistens still bleiben oder mit einer Standardkonfiguration arbeiten.

Ein bisschen schwieriger wird es, wenn du gemeinsame Abhängigkeiten haben willst. Eine Möglichkeit, gemeinsame Abhängigkeiten zu erreichen, ist die Verwendung von Projektreferenzen und zusammengesetzten Projekten. Das bedeutet, dass du deinen gemeinsam genutzten Code in einen eigenen Ordner extrahierst, TypeScript aber mitteilst, dass dieser ein Abhängigkeitsprojekt eines anderen Projekts sein soll.

Erstelle einen gemeinsamen Ordner auf der gleichen Ebene wie Client und Server. Erstelle eine tsconfig.json in shared mit diesem Inhalt:

// shared/tsconfig.json
{
    "compilerOptions": {
      "composite": true,
      "target": "ESNext",
      "module": "ESNext",
      "rootDir": "../shared/",
      "moduleResolution": "Node",
      "types": [],
      "declaration": true,
      "outDir": "../dist/shared",
      "esModuleInterop": true,
      "forceConsistentCasingInFileNames": true,
      "strict": true,
      "skipLibCheck": true
    },
  }

Zwei Dinge fallen wieder auf:

  • Das Flag composite ist auf true gesetzt. Dadurch können andere Projekte auf dieses Projekt verweisen.

  • Das declaration Flag ist auch auf true gesetzt. Dadurch werden d.ts-Dateien aus deinem Code erzeugt, damit andere Projekte die Typinformationen nutzen können.

Um sie in deinen Client- und Servercode einzubinden, füge diese Zeile zu client/tsconfig.json und server/tsconfig.json hinzu:

// server/tsconfig.json
// client/tsconfig.json
{
  "compilerOptions": {
    // Same as before
  },
  "references": [
    { "path": "../shared/tsconfig.json" }
  ]
}

Und schon bist du bereit. Du kannst gemeinsame Abhängigkeiten schreiben und sie in deinenClient- und Servercode einbauen.

Es gibt jedoch eine Einschränkung. Das funktioniert gut, wenn du zum Beispiel nur Modelle und Typinformationen teilst, aber sobald du tatsächliche Funktionen teilst, wirst du feststellen, dass die beiden unterschiedlichen Modulsysteme (CommonJS in Node, ECMAScript-Module im Browser) nicht in einer kompilierten Datei vereint werden können. Entweder erstellst du ein ESNext-Modul und kannst es nicht in CommonJS-Code importieren oder du erstellst CommonJS-Code und kannst ihn nicht im Browser importieren.

Es gibt zwei Dinge, die du tun kannst:

  • Kompiliere nach CommonJS und lass einen Bundler die Arbeit der Modulauflösung für den Browser übernehmen.

  • Kompiliere zu ECMAScript-Modulen und schreibe moderne Node.js-Anwendungen, die auf ECMAScript-Modulen basieren. Siehe Rezept 1.9 für weitere Informationen.

Da du neu anfängst, empfehle ich dir dringend die zweite Option.

1.7 Tests einrichten

Problem

Du willst Tests schreiben, aber die Globals für Test-Frameworks stören deinen Produktionscode.

Lösung

Erstelle eine separate tsconfig für Entwicklung und Build und schließe in letzterer alle Testdateien aus.

Diskussion

Im JavaScript- und Node.js-Ökosystem gibt es eine Vielzahl von Unit-Testing-Frameworks und Test-Runnern. Sie unterscheiden sich im Detail, haben unterschiedliche Meinungen oder sind auf bestimmte Bedürfnisse zugeschnitten. Manche von ihnen sind einfach hübscher als andere.

Während Testrunner wie Ava auf den Import von Modulen angewiesen sind, um das Framework in den Anwendungsbereich zu bekommen, bieten andere eine Reihe von Globals. Nimm zum Beispiel Mocha:

import assert from "assert";
import { add } from "..";

describe("Adding numbers", () => {
  it("should add two numbers", () => {
    assert.equal(add(2, 3), 5);
  });
});

assert stammt aus der in Node.js eingebauten Assertion-Bibliothek, aber describe, it und viele andere sind Globals, die von Mocha bereitgestellt werden. Sie sind auch nur vorhanden, wenn die Mocha CLI läuft.

Das ist eine kleine Herausforderung für dein Typen-Setup, denn diese Funktionen sind notwendig, um Tests zu schreiben, aber sie sind nicht verfügbar, wenn du deine eigentliche Anwendung ausführst.

Die Lösung ist, zwei verschiedene Konfigurationsdateien zu erstellen: eine normale tsconfig.json für die Entwicklung, die dein Editor übernehmen kann (erinnere dich an Rezept 1.6) und eine separate tsconfig.build.json, die du verwendest, wenn du deine Anwendung kompilieren willst.

Die erste enthält alle Globals, die du brauchst, einschließlich der Typen für Mocha; die zweite stellt sicher, dass keine Testdatei in deiner Kompilierung enthalten ist.

Lass uns das Schritt für Schritt durchgehen. Wir sehen uns Mocha als Beispiel an, aber andere Testrunner, die Globals bereitstellen, wie Jest, funktionieren genauso.

Installiere zunächst Mocha und seine Typen:

$ npm install --save-dev mocha @types/mocha @types/node

Erstelle eine neue tsconfig.base.json. Da die einzigen Unterschiede zwischen Entwicklung und Build die einzubindenden Dateien und die aktivierten Bibliotheken sind, solltest du alle anderen Compiler-Einstellungen in einer Datei haben, die du für beide wiederverwenden kannst. Eine Beispieldatei für eine Node.js-Anwendung würde wie folgt aussehen:

// tsconfig.base.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "outDir": "./dist",
    "skipLibCheck": true
  }
}

Die Quelldateien sollten sich in src befinden, die Testdateien in einem angrenzenden Ordner test. Mit den Einstellungen, die du in diesem Rezept erstellst, kannst du auch Dateien mit der Endung .test.ts überall in deinem Projekt erstellen.

Erstelle eine neue tsconfig.json mit deiner grundlegenden Entwicklungskonfiguration. Diese wird für das Feedback des Editors und für die Ausführung von Tests mit Mocha verwendet. Du erweiterst die Grundeinstellungen aus tsconfig.base.json und teilst TypeScript mit, welche Ordnerfür die Kompilierung herangezogen werden sollen:

// tsconfig.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "types": ["node", "mocha"],
    "rootDirs": ["test", "src"]
  }
}

Beachte, dass du types für Node und Mocha hinzufügst. Die Eigenschaft types legt fest, welche Globals verfügbar sind, und in der Entwicklungseinstellung hast du beide.

Außerdem kann es mühsam sein, deine Tests zu kompilieren, bevor du sie ausführst. Es gibt Abkürzungen, die dir helfen. So führt ts-node zum Beispiel deine lokale Node.js-Installation aus und kompiliert TypeScript zuerst im Arbeitsspeicher:

$ npm install --save-dev ts-node
$ npx mocha -r ts-node/register tests/*.ts

Nachdem du die Entwicklungsumgebung eingerichtet hast, ist es Zeit für die Build-Umgebung. Erstelle eine tsconfig.build.json. Sie sieht ähnlich aus wie tsconfig.json, aber du wirst den Unterschied sofort erkennen:

// tsconfig.build.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "types": ["node"],
    "rootDirs": ["src"]
  },
  "exclude": ["**/*.test.ts", "**/test/**"]
}

Zusätzlich zur Änderung von types und rootDirs legst du fest, welche Dateien von der Typprüfung und Kompilierung ausgeschlossen werden sollen. Du verwendest Platzhaltermuster, die alle Dateien mit der Endung .test.ts ausschließen, die sich in Testordnern befinden. Je nach deinem Geschmack kannst du auch .spec.ts oder spec-Ordner zu diesem Array hinzufügen.

Kompiliere dein Projekt, indem du auf die richtige JSON-Datei verweist:

$ npx tsc -p tsconfig.build.json

Du wirst feststellen, dass in den Ergebnisdateien (die sich unter dist befinden) keine Testdatei zu finden ist. Außerdem kannst du zwar immer noch auf describe und it zugreifen, wenn du deine Quelldateien bearbeitest, aber du bekommst einen Fehler, wenn du versuchst zu kompilieren:

$ npx tsc -p tsconfig.build.json

src/index.ts:5:1 - error TS2593: Cannot find name 'describe'.
Do you need to install type definitions for a test runner?
Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`
and then add 'jest' or 'mocha' to the types field in your tsconfig.

5 describe("this does not work", () => {})
  ~~~~~~~~


Found 1 error in src/index.ts:5

Wenn du deine Globals im Entwicklungsmodus nicht verschmutzen willst, kannst du eine ähnliche Einstellung wie in Rezept 1.6 wählen, aber dann kannst du keine Tests neben deinen Quelldateien schreiben.

Schließlich kannst du dich auch für einen Test Runner entscheiden, der das Modulsystem bevorzugt.

1.8 ECMAScript-Module von URLs abtippen

Problem

Du willst ohne Bundler arbeiten und die Möglichkeiten des Browsers zum Laden von Modulen für deine App nutzen, aber du willst trotzdem alle Typinformationen haben.

Lösung

Setze target und module in den Compiler-Optionen deiner tsconfigauf esnext und verweise auf deine Module mit einer .js-Erweiterung. Installiere außerdem Typen über NPM in Abhängigkeiten und verwende die Eigenschaft path in deiner tsconfig, um TypeScript mitzuteilen, wo es nach Typen suchen soll:

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "paths": {
      "https://esm.sh/lodash@4.17.21": [
        "node_modules/@types/lodash/index.d.ts"
      ]
    }
  }
}

Diskussion

Moderne Browser unterstützen das Laden von Modulen von Haus aus. Anstatt deine App in einen kleineren Satz von Dateien zu bündeln, kannst du die rohen JavaScript-Dateien direkt verwenden.

Content Delivery Networks (CDNs) wie esm.sh, unpkg und andere wurden entwickelt, um Node-Module und JavaScript-Abhängigkeiten als URLs zu verteilen, die von nativen ECMAScript-Modulen geladen werden können.

Mit richtigem Caching und modernem HTTP werden ECMAScript-Module zu einer echten Alternative für Apps.

TypeScript enthält keinen modernen Bundler, so dass du ohnehin ein zusätzliches Tool installieren müsstest. Wenn du dich aber für das Modul entscheidest, gibt es ein paar Dinge zu beachten, wenn du mit TypeScript arbeitest.

Was du erreichen willst, ist, dass du import und export Anweisungen in TypeScript schreibst, aber die Syntax für das Laden von Modulen beibehältst und die Auflösung der Module dem Browser überlässt:

// File module.ts
export const obj = {
  name: "Stefan",
};

// File index.ts
import { obj } from "./module";

console.log(obj.name);

Um dies zu erreichen, musst du TypeScript anweisen:

  1. Kompiliere zu einer ECMAScript-Version, die Module versteht

  2. Verwende die ECMAScript-Modulsyntax für die Erzeugung von Modulcode

Aktualisiere zwei Eigenschaften in deiner tsconfig.json:

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext"
  }
}

module teilt TypeScript mit, wie import- und export-Anweisungen umgewandelt werden sollen. Die Standardeinstellung wandelt das Laden von Modulen in CommonJS um, wie in Rezept 1.2 beschrieben. Wenn du module auf esnext setzt, wird das Laden von ECMAScript-Modulen verwendet und die Syntax bleibt erhalten.

target teilt TypeScript die ECMAScript-Version mit, in die du deinen Code transpilieren willst. Einmal im Jahr gibt es eine neue ECMAScript-Version mit neuen Funktionen. Wenn du target auf esnext setzt, wird immer die neueste ECMAScript-Version verwendet.

Je nach deinen Kompatibilitätszielen solltest du diese Eigenschaft auf die ECMAScript-Version setzen, die mit den Browsern kompatibel ist, die du unterstützen willst. Dies ist in der Regel eine Version mit einer Jahreszahl (z. B. es2015, es2016, es2017, usw.). ECMAScript-Module funktionieren mit jeder Version ab es2015. Wenn du dich für eine ältere Version entscheidest, kannst du ECMAScript-Module nicht nativ in den Browser laden.

Das Ändern dieser Compiler-Optionen bewirkt bereits eine wichtige Sache: Es lässt die Syntax intakt. Ein Problem tritt auf, wenn du deinen Code ausführen willst.

Normalerweise verweisen Import-Anweisungen in TypeScript auf Dateien ohne eine Erweiterung. Du schreibst import { obj } from "./module" und lässt .ts weg. Wenn du kompilierst, fehlt diese Erweiterung immer noch. Aber der Browser braucht eine Erweiterung, um tatsächlich auf die entsprechende JavaScript-Datei zu verweisen.

Die Lösung: Füge eine .js-Erweiterung hinzu, auch wenn du bei der Entwicklung auf eine .ts-Datei zeigst. TypeScript ist schlau genug, um das zu erkennen:

// index.ts

// This still loads types from 'module.ts', but keeps
// the reference intact once we compile it.
import { obj } from './module.js';

console.log(obj.name);

Das ist alles, was du für die Module deines Projekts brauchst!

Viel interessanter wird es, wenn du Abhängigkeiten nutzen willst. Wenn du nativ arbeitest, möchtest du vielleicht Module von einem CDN laden, wie esm.sh:

import _ from "https://esm.sh/lodash@4.17.21"
//             ^- Error 2307

const result = _.flattenDeep([1, [2, [3, [4]], 5]]);

console.log(result);

TypeScript gibt die folgende Fehlermeldung aus: "Cannot find module ... or its corresponding type declarations. (2307)"

Die Modulauflösung von TypeScript funktioniert, wenn die Dateien auf deiner Festplatte liegen, nicht auf einem Server über HTTP. Um die Informationen zu erhalten, die wir brauchen, müssen wir TypeScript eine eigene Auflösung zur Verfügung stellen.

Auch wenn wir Abhängigkeiten von URLs laden, liegen die Typinformationen für diese Abhängigkeiten bei NPM. Für lodash kannst du die Typinformationen von Definitely Typed installieren:

$ npm install -D @types/lodash

Bei Abhängigkeiten, für die es eigene Typen gibt, kannst du die Abhängigkeiten direkt installieren:

$ npm install -D preact

Sobald die Typen installiert sind, kannst du TypeScript mit der Eigenschaft path in deinen Compiler-Optionen mitteilen, wie die URL aufgelöst werden soll:

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "paths": {
      "https://esm.sh/lodash@4.17.21": [
        "node_modules/@types/lodash/index.d.ts"
      ]
    }
  }
}

Achte darauf, dass du auf die richtige Datei zeigst!

Es gibt auch einen Ausweg, wenn du keine Typisierungen verwenden willst oder wenn du einfach keine Typisierungen finden kannst. In TypeScript können wir any verwenden, um die Typüberprüfung absichtlich zu deaktivieren. Bei Modulen können wir etwas ganz Ähnliches tun: den TypeScript-Fehler ignorieren:

// @ts-ignore
import _ from "https://esm.sh/lodash@4.17.21"

ts-ignore entfernt die nächste Zeile aus der Typprüfung und kann überall dort verwendet werden, wo du Typfehler ignorieren willst (siehe Rezept 1.4). Das bedeutet zwar, dass du keine Typinformationen für deine Abhängigkeiten erhältst und du auf Fehler stoßen könntest, aber es könnte die ultimative Lösung für nicht gewartete, alte Abhängigkeiten sein, die du gerade brauchst, für die du aber keine Typen finden wirst.

1.9 Laden verschiedener Modultypen in Node

Problem

Du möchtest ECMAScript-Module in Node.js und die CommonJS-Interoperabilitätsfunktion für Bibliotheken nutzen.

Lösung

Setze die Modulauflösung von TypeScript auf "nodeNext" und nenne deine Dateien .mts oder .cts.

Diskussion

Mit dem Aufkommen von Node.js hat sich das CommonJS-Modulsystem zu einem der beliebtesten Modulsysteme im JavaScript-Ökosystem entwickelt.

Die Idee ist einfach und effektiv: Definiere Exporte in einem Modul und fordere siein einem anderen:

// person.js
function printPerson(person) {
  console.log(person.name);
}

exports = {
  printPerson,
};

// index.js
const person = require("./person");
person.printPerson({ name: "Stefan", age: 40 });

Dieses System hatte einen großen Einfluss auf ECMAScript-Module und ist auch der Standard für die Modulauflösung und den Transpiler von TypeScript. Wenn du dir die Syntax der ECMAScript-Module in Beispiel 1-1 ansiehst, kannst du sehen, dass die Schlüsselwörter verschiedene Transpilierungen zulassen. Das bedeutet, dass mit der Moduleinstellung commonjs deine import und export Anweisungen in require und exports transpiliert werden.

Beispiel 1-1. Verwendung des ECMAScript-Modulsystems
// person.ts
type Person = {
  name: string;
  age: number;
};

export function printPerson(person) {
  console.log(person.name);
}

// index.ts
import * as person from "./person";
person.printPerson({ name: "Stefan", age: 40 });

Mit der Stabilisierung der ECMAScript-Module hat auch Node.js begonnen, sie zu übernehmen. Auch wenn die Grundlagen beider Modulsysteme sehr ähnlich zu sein scheinen, gibt es doch einige Unterschiede im Detail, z. B. bei der Handhabung von Standard-Exporten oder dem asynchronen Laden von ECMAScript-Modulen.

Da es keine Möglichkeit gibt, beide Modulsysteme gleich zu behandeln, aber mit unterschiedlicher Syntax, haben die Node.js-Maintainer beschlossen, beiden Systemen Raum zu geben und unterschiedliche Dateiendungen zu vergeben, um den bevorzugten Modultyp zu kennzeichnen. Tabelle 1-1 zeigt die verschiedenen Endungen, wie sie in TypeScript benannt sind, wie TypeScript sie kompiliert und was sie importieren können. Dank der CommonJS-Interoperabilität ist es möglich, CommonJS-Module aus ECMAScript-Modulen zu importieren, aber nicht andersherum.

Tabelle 1-1. Modul-Endungen und was sie importieren
Ende TypeScript Kompiliert zu Kann importieren

.js

.ts

CommonJS

.js, .cjs

.cjs

.cts

CommonJS

.js, .cjs

.mjs

.mts

ES-Module

.js, .cjs, .mjs

Bibliotheksentwickler, die auf NPM veröffentlichen, erhalten in ihrer package.json-Datei zusätzliche Informationen, um den Haupttyp eines Pakets (module oder commonjs) anzugeben und auf eine Liste von Hauptdateien oder Fallbacks zu verweisen, damit Modullader die richtige Datei auswählen können:

// package.json
{
  "name": "dependency",
  "type": "module",
  "exports": {
     ".": {
        // Entry-point for `import "dependency"` in ES Modules
        "import": "./esm/index.js",
        // Entry-point for `require("dependency") in CommonJS
        "require": "./commonjs/index.cjs",
     },
  },
  // CommonJS Fallback
  "main": "./commonjs/index.cjs"
}

In TypeScript schreibst du hauptsächlich in der ECMAScript-Modulsyntax und lässt den Compiler entscheiden, welches Modulformat am Ende erstellt werden soll. Jetzt gibt es möglicherweise zwei: CommonJS- und ECMAScript-Module.

Um beides zu ermöglichen, kannst du die Modulauflösung in deiner tsconfig.json auf NodeNext setzen:

{
  "compilerOptions": {
    "module": "NodeNext"
    // ...
  }
}

Mit diesem Flag nimmt TypeScript die richtigen Module, wie in deiner dependencies package.json beschrieben, erkennt die Endungen .mts und .cts und folgt Tabelle 1-1 für Modulimporte.

Für dich als Entwickler gibt es Unterschiede beim Importieren von Dateien. Da CommonJS keine Endungen beim Importieren verlangt, unterstützt TypeScript weiterhin Importe ohne Endungen. Das Beispiel in Beispiel 1-1 funktioniert immer noch, wenn du nur CommonJS verwendest.

Der Import mit Dateiendungen, genau wie in Rezept 1.8, ermöglicht es, Module sowohl in ECMAScript- als auch in CommonJS-Module zu importieren:

// index.mts
import * as person from "./person.js"; // works in both
person.printPerson({ name: "Stefan", age: 40});

Sollte die CommonJS-Interoperabilität nicht funktionieren, kannst du jederzeit auf eine require Anweisung zurückgreifen. Füge "node" als globale Typen zu deinen Compiler-Optionen hinzu:

// tsconfig.json
{
  "compilerOptions": {
    "module": "NodeNext",
    "types": ["node"],
  }
}

Dann importiere mit dieser TypeScript-spezifischen Syntax:

// index.mts
import person = require("./person.cjs");

person.printPerson({ name: "Stefan", age: 40 });

In einem CommonJS-Modul ist dies nur ein weiterer require -Aufruf; in ECMAScript-Modulen beinhaltet dies Node.js-Hilfsfunktionen:

// compiled index.mts
import { createRequire as _createRequire } from "module";
const __require = _createRequire(import.meta.url);
const person = __require("./person.cjs");
person.printPerson({ name: "Stefan", age: 40 });

Beachte, dass dies die Kompatibilität mit Nicht-Node.js-Umgebungen wie dem Browser einschränkt, aber es könnte eventuell Interoperabilitätsprobleme lösen.

1.10 Arbeiten mit Deno und Abhängigkeiten

Problem

Du willst TypeScript mit Deno verwenden, einer modernen JavaScript-Laufzeitumgebung für Anwendungen außerhalb des Browsers.

Lösung

Das ist ganz einfach: TypeScript ist eingebaut.

Diskussion

Deno ist eine moderne JavaScript-Laufzeitumgebung, die von denselben Leuten entwickelt wurde, die auch Node.js entwickelt haben. Deno ähnelt Node.js in vielerlei Hinsicht, weist aber auch erhebliche Unterschiede auf:

  • Deno setzt für seine wichtigsten APIs auf Webplattform-Standards, was bedeutet, dass es einfacher ist, Code vom Browser auf den Server zu portieren.

  • Sie erlaubt den Zugriff auf das Dateisystem oder das Netzwerk nur, wenn du sie explizit aktivierst.

  • Es verwaltet Abhängigkeiten nicht über eine zentrale Registry, sondern - wiederum in Anlehnung an Browser-Funktionen - über URLs.

Oh, und es kommt mit integrierten Entwicklungswerkzeugen und TypeScript!

Deno ist das Tool mit der niedrigsten Hürde, wenn du TypeScript ausprobieren willst. Du musst kein anderes Tool herunterladen (der tsc Compiler ist bereits integriert) und brauchst keine TypeScript-Konfigurationen. Du schreibst .ts-Dateien, und Deno erledigt den Rest:

// main.ts
function sayHello(name: string) {
  console.log(`Hello ${name}`);
}

sayHello("Stefan");
$ deno run main.ts

Deno's TypeScript kann alles, was tsc kann, und es wird mit jedem Deno-Update aktualisiert. Allerdings gibt es einige Unterschiede, wenn du es konfigurieren willst.

Erstens unterscheidet sich die Standardkonfiguration von der Standardkonfiguration, die von tsc --init ausgegeben wird. Die Flags für den strikten Modus sind anders gesetzt, und sie enthält Unterstützung für React (auf der Serverseite!).

Um Änderungen an der Konfiguration vorzunehmen, solltest du eine deno.json-Datei in deinem Stammverzeichnis erstellen. Deno übernimmt diese Datei automatisch, es sei denn, du sagst, dass es nicht so sein soll. deno.json enthält verschiedene Konfigurationen für die Deno-Laufzeit, darunter auch TypeScript-Compiler-Optionen:

{
  "compilerOptions": {
    // Your TSC compiler options
  },
  "fmt": {
    // Options for the auto-formatter
  },
  "lint": {
    // Options for the linter
  }
}

Weitere Möglichkeiten findest du auf der Deno-Website.

Auch die Standardbibliotheken sind unterschiedlich. Obwohl Deno Webplattform-Standards unterstützt und browser-kompatible APIs hat, muss es einige Abstriche machen, weil es keine grafische Benutzeroberfläche gibt. Deshalb kollidieren einige Typen, z. B. die DOM-Bibliothek, mit dem, was Deno bietet.

Einige interessante Bibliotheken sind:

  • deno.ns, der standardmäßige Deno-Namensraum

  • deno.window, das globale Objekt für Deno

  • deno.worker, die Entsprechung für Web Worker in der Deno-Laufzeitumgebung

DOM und Subsets sind in Deno enthalten, aber sie sind nicht standardmäßig aktiviert. Wenn deine Anwendung sowohl auf den Browser als auch auf Deno abzielt, konfiguriere Deno so, dass alle Browser- und Deno-Bibliotheken enthalten sind:

// deno.json
{
  "compilerOptions": {
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
  }
}

Aleph.js ist ein Beispiel für ein Framework, das sowohl auf Deno als auch auf den Browser abzielt.

Ein weiterer Unterschied zu Deno ist die Art und Weise, wie die Typinformationen für Abhängigkeiten verteilt werden. Externe Abhängigkeiten werden bei Deno über URLs aus einem CDN geladen. Deno selbst hostet seine Standardbibliothek unter https://deno.land/std.

Du kannst aber auch CDNs wie esm.sh oder unpkg verwenden, wie in Rezept 1.8. Diese CDNs verteilen Typen, indem sie einen X-TypeScript-Types Header mit der HTTP-Anfrage senden, der zeigt, dass Deno die Typdeklarationen laden soll. Das gilt auch für Abhängigkeiten, die keine eigenen Typendeklarationen haben, sondern aufDefinitely Typed angewiesen sind.

In dem Moment, in dem du deine Abhängigkeit installierst, holt Deno also nicht nur die Quelldateien, sondern auch alle Typinformationen.

Wenn du eine Abhängigkeit nicht von einem CDN lädst, sondern sie lokal hast, kannst du auf eine Typdeklarationsdatei verweisen, sobald du die Abhängigkeit importierst:

// @deno-types="./charting.d.ts"
import * as charting from "./charting.js";

oder füge einen Verweis auf die Typisierungen in der Bibliothek selbst ein:

// charting.js
/// <reference types="./charting.d.ts" />

Diese Referenz wird auch als Triple-Slash-Direktive bezeichnet und ist ein TypeScript-Feature, kein Deno-Feature. Es gibt verschiedene Triple-Slash-Direktiven, die meist für Modul-Abhängigkeitssysteme vor ECMAScript verwendet werden. Die Dokumentation gibt einen sehr guten Überblick. Wenn du dich an ECMAScript-Module hältst, wirst du wahrscheinlich keine Triple-Slash-Direktiven verwenden.

1.11 Vordefinierte Konfigurationen verwenden

Problem

Du willst TypeScript für ein bestimmtes Framework oder eine bestimmte Plattform nutzen, weißt aber nicht, wo du mit der Konfiguration anfangen sollst.

Lösung

Verwende eine vordefinierte Konfiguration aus tsconfig/bases und erweitere sie von dort aus.

Diskussion

So wie Definitely Typed von der Community gepflegte Typdefinitionen für beliebte Bibliotheken bereitstellt, enthält tsconfig/bases eine Reihe von von der Community gepflegten Empfehlungen für TypeScript-Konfigurationen, die du als Ausgangspunkt für dein eigenes Projekt verwenden kannst. Dazu gehören Frameworks wie Ember.js, Svelte oder Next.js sowie JavaScript-Laufzeiten wie Node.js und Deno.

Die Konfigurationsdateien sind auf ein Minimum reduziert und befassen sich hauptsächlich mit empfohlenen Bibliotheken, Modulen und Zieleinstellungen sowie einer Reihe von Strict Mode Flags, die für die jeweilige Umgebung sinnvoll sind.

Dies ist zum Beispiel die empfohlene Konfiguration für Node.js 18, mit einer empfohlenen strict mode Einstellung und mit ECMAScript Modulen:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Node 18 + ESM + Strictest",
  "compilerOptions": {
    "lib": [
      "es2022"
    ],
    "module": "es2022",
    "target": "es2022",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "allowUnusedLabels": false,
    "allowUnreachableCode": false,
    "exactOptionalPropertyTypes": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "importsNotUsedAsValues": "error",
    "checkJs": true
  }
}

Um diese Konfiguration zu verwenden, installiere sie über NPM:

$ npm install --save-dev @tsconfig/node18-strictest-esm

und verdrahte sie in deiner eigenen TypeScript-Konfiguration:

{
  "extends": "@tsconfig/node18-strictest-esm/tsconfig.json",
  "compilerOptions": {
    // ...
  }
}

Dadurch werden alle Einstellungen aus der vordefinierten Konfiguration übernommen. Jetzt kannst du deine eigenen Eigenschaften festlegen, z. B. das Stammverzeichnis und die Verzeichnisse.

1 Objekte, die einer const Bindung zugewiesen sind, können immer noch Werte und Eigenschaften und damit auch ihre Typen ändern.

2 TypeScript funktioniert auch in anderen JavaScript-Laufzeiten, wie Deno und dem Browser, aber sie sind nicht als Hauptziel gedacht.

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