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
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.
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
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
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
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.
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.
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
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'
)
;
const
hostname
=
'127.0.0.1'
;
const
port
=
process
.
env
.
PORT
||
3000
;
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
}
/
`
)
;
}
)
;
Obwohl es sich zweifellos um JavaScript handelt, sind einige Dinge einzigartig für Node.js:
"http"
ist ein eingebautes Node.js-Modul für alles, was mit HTTP zu tun hat. Es wird überrequire
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.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.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
`
;
document
.
querySelector
(
'button'
)
?
.
addEventListener
(
"click"
,
(
)
=>
{
console
.
log
(
msg
)
;
}
)
;
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).
JavaScript im Browser ist für die Interaktion mit UI-Ereignissen gedacht. Das
document
Objekt und die Idee einesquerySelector
, 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.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 aufcommonjs
gesetzt, das ursprüngliche Node.js-Modulsystem. Alleimport
undexport
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ürrequire
,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 Eigenschaftlib
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 auftrue
gesetzt. Dadurch können andere Projekte auf dieses Projekt verweisen. -
Das
declaration
Flag ist auch auftrue
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
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
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:
-
Kompiliere zu einer ECMAScript-Version, die Module versteht
-
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
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.
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
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
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.
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.