Kapitel 1. TypeScript kennenlernen
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Bevor wir in die Details eintauchen, hilft dir dieses Kapitel, das große Ganze von TypeScript zu verstehen. Was ist es und wie solltest du darüber denken? Wie verhält es sich zu JavaScript? Sind die Typen nullbar oder nicht? Was hat es mit any
auf sich? Und Enten?
TypeScript ist eine ungewöhnliche Sprache, da sie weder in einem Interpreter läuft (wie Python und Ruby) noch zu einer niedrigeren Sprache kompiliert wird (wie Java und C). Stattdessen wird sie in eine andere Hochsprache, JavaScript, kompiliert. Es ist dieses JavaScript, das ausgeführt wird, nicht dein TypeScript. Deshalb ist es wichtig, die Beziehung zwischen TypeScript und JavaScript zu verstehen, damit du ein effektiver TypeScript-Entwickler werden kannst.
Das Typsystem von TypeScript hat auch einige ungewöhnliche Aspekte, die du kennen solltest. In späteren Kapiteln wird das Typsystem noch viel ausführlicher behandelt, aber in diesem Kapitel werden einige der wichtigsten Highlights angesprochen.
Du solltest dieses Kapitel lesen, auch wenn du schon viel TypeScript geschrieben hast. Es wird dir helfen, ein richtiges mentales Modell davon zu entwickeln, was TypeScript ist und wie das Typsystem funktioniert, und es wird vielleicht einige Missverständnisse ausräumen, von denen du gar nicht wusstest, dass du sie hast.
Punkt 1: Verstehe die Beziehung zwischen TypeScript und JavaScript
Wenn du lange mit TypeScript arbeitest, wirst du unweigerlich den Satz "TypeScript ist eine Obermenge von JavaScript" oder "TypeScript ist eine typisierte Obermenge von JavaScript" hören. Aber was bedeutet das genau? Und wie ist die Beziehung zwischen TypeScript und JavaScript? Da diese Sprachen so eng miteinander verbunden sind, ist ein gutes Verständnis ihrer Beziehung zueinander die Grundlage für eine gute Nutzung von TypeScript.
A
ist eine "Obermenge" von B
, wenn alles, was in B
steht, auch in A
steht. TypeScript ist ein Superset von JavaScript im syntaktischen Sinne: Solange dein JavaScript-Programm keine Syntaxfehler hat, ist es auch ein TypeScript-Programm. Es ist sehr wahrscheinlich, dass der TypeScript-Typ-Checker einige Probleme in deinem Code aufzeigt. Aber das ist ein unabhängiges Problem. TypeScript wird deinen Code trotzdem analysieren und JavaScript ausgeben. (Dies ist ein weiterer wichtiger Teil der Beziehung. Darauf gehen wir in Punkt 3 näher ein.)
TypeScript-Dateien verwenden eine .ts-Erweiterung und nicht die .js-Erweiterung einer JavaScript-Datei.1 Das bedeutet aber nicht, dass TypeScript eine völlig andere Sprache ist! Da TypeScript eine Obermenge von JavaScript ist, ist der Code in deinen .js-Dateien bereits TypeScript. Das Umbenennen von main.js in main.ts ändert daran nichts.
Das ist enorm hilfreich, wenn du eine bestehende JavaScript-Codebasis zu TypeScript migrierst. Das bedeutet, dass du deinen Code nicht in einer anderen Sprache neu schreiben musst, um TypeScript zu verwenden und die Vorteile zu nutzen, die es bietet. Das wäre nicht der Fall, wenn du deinen JavaScript-Code in einer Sprache wie Java umschreiben würdest. Dieser sanfte Migrationspfad ist eine der besten Eigenschaften von TypeScript. Zu diesem Thema wird es in Kapitel 10 noch viel mehr zu sagen geben.
Alle JavaScript-Programme sind TypeScript-Programme, aber das Gegenteil ist nicht der Fall: Es gibt TypeScript-Programme, die keine JavaScript-Programme sind. Das liegt daran, dass TypeScript eine zusätzliche Syntax für die Angabe von Typen enthält. (Es gibt auch noch einige andere Syntaxelemente, die vor allem aus historischen Gründen hinzugefügt wurden. Siehe Punkt 72.)
Dies ist zum Beispiel ein gültiges TypeScript-Programm:
function
greet
(
who
:
string
)
{
console
.
log
(
'Hello'
,
who
);
}
Aber wenn du das durch ein Programm wie node
laufen lässt, das JavaScript erwartet, bekommst du einen Fehler:
function greet(who: string) { ^ SyntaxError: Unexpected token :
Die : string
ist eine Typ-Annotation, die spezifisch für TypeScript ist. Sobald du eine solche Annotation verwendest, gehst du über einfaches JavaScript hinaus (siehe Abbildung 1-1).
Das soll nicht heißen, dass TypeScript keine Vorteile für einfache JavaScript-Programme bietet. Das tut es! Zum Beispiel dieses JavaScript-Programm:
let
city
=
'new york city'
;
console
.
log
(
city
.
toUppercase
());
wird einen Fehler auslösen, wenn du es ausführst:
TypeError: city.toUppercase is not a function
In diesem Programm gibt es keine Typ-Annotationen, aber der TypeScript-Typ-Checker kann das Problem trotzdem erkennen:
let
city
=
'new york city'
;
console
.
log
(
city
.
toUppercase
());
// ~~~~~~~~~~~ Property 'toUppercase' does not exist on type
// 'string'. Did you mean 'toUpperCase'?
Du musst TypeScript nicht sagen, dass der Typ von city
string
ist: Es leitet ihn aus dem Anfangswert von ab. Die Typinferenz ist ein wichtiger Bestandteil von TypeScript, und in Kapitel 3 erfährst du, wie du sie gut nutzen kannst.
Eines der Ziele des TypeScript-Typensystems ist es, Code zu erkennen, der zur Laufzeit eine Ausnahme auslöst, ohne dass du deinen Code ausführen musst. Wenn TypeScript als "statisches" Typsystem bezeichnet wird, bezieht sich das auf diese Fähigkeit. Der Typprüfer kann Code, der Ausnahmen auslöst, nicht immer erkennen, aber er wird es versuchen.
Auch wenn dein Code keine Ausnahme auslöst, kann es sein, dass er nicht das tut, was du beabsichtigst. TypeScript versucht, auch einige dieser Probleme abzufangen. Zum Beispiel diesesJavaScript-Programm:
const
states
=
[
{
name
:
'Alabama'
,
capital
:
'Montgomery'
},
{
name
:
'Alaska'
,
capital
:
'Juneau'
},
{
name
:
'Arizona'
,
capital
:
'Phoenix'
},
// ...
];
for
(
const
state
of
states
)
{
console
.
log
(
state
.
capitol
);
}
wird protokolliert:
undefined undefined undefined
Huch! Was ist falsch gelaufen? Dieses Programm ist gültiges JavaScript (und damit TypeScript). Und es lief ohne Fehler. Aber es hat eindeutig nicht das getan, was du beabsichtigt hast. Auch ohne das Hinzufügen von Typ-Annotationen kann der TypeScript-Typ-Checker den Fehler erkennen und dir einen hilfreichen Vorschlag machen:
for
(
const
state
of
states
)
{
console
.
log
(
state
.
capitol
);
// ~~~~~~~ Property 'capitol' does not exist on type
// '{ name: string; capital: string; }'.
// Did you mean 'capital'?
}
Wir meinten in der Tat capital
mit einem "a". Staaten und Länder haben Hauptstädte ("a"), während die Parlamente in Kapitolgebäuden ("o") tagen.
TypeScript kann zwar auch Fehler finden, wenn du keine Typ-Annotationen angibst, aber wenn du sie angibst, ist es in der Lage, eine viel gründlichere Arbeit zu leisten. Das liegt daran, dass TypeScript durch die Typ-Annotationen erfährt, was deine Absicht ist, und dadurch kann es erkennen, wo das Verhalten deines Codes nicht mit deiner Absicht übereinstimmt. Was wäre zum Beispiel, wenn du den Rechtschreibfehler capital
/capitol
im vorherigen Beispiel rückgängig gemacht hättest?
const
states
=
[
{
name
:
'Alabama'
,
capitol
:
'Montgomery'
},
{
name
:
'Alaska'
,
capitol
:
'Juneau'
},
{
name
:
'Arizona'
,
capitol
:
'Phoenix'
},
// ...
];
for
(
const
state
of
states
)
{
console
.
log
(
state
.
capital
);
// ~~~~~~~ Property 'capital' does not exist on type
// '{ name: string; capitol: string; }'.
// Did you mean 'capitol'?
}
Der Fehler, der vorher so hilfreich war, ist jetzt genau falsch! Das Problem ist, dass du dieselbe Eigenschaft auf zwei verschiedene Arten geschrieben hast, und TypeScript weiß nicht, welche richtig ist. Es kann zwar raten, aber das ist nicht immer richtig. Die Lösung ist, dass du deine Absicht klarstellst, indem du den Typ von states
explizit deklarierst:
interface
State
{
name
:
string
;
capital
:
string
;
}
const
states
:
State
[]
=
[
{
name
:
'Alabama'
,
capitol
:
'Montgomery'
},
// ~~~~~~~
{
name
:
'Alaska'
,
capitol
:
'Juneau'
},
// ~~~~~~~
{
name
:
'Arizona'
,
capitol
:
'Phoenix'
},
// ~~~~~~~ Object literal may only specify known properties,
// but 'capitol' does not exist in type 'State'.
// Did you mean to write 'capital'?
// ...
];
for
(
const
state
of
states
)
{
console
.
log
(
state
.
capital
);
}
Jetzt stimmen die Fehler mit dem Problem überein und die vorgeschlagene Lösung ist korrekt. Indem du deine Absicht klargestellt hast, hast du TypeScript auch geholfen, andere potenzielle Probleme zu erkennen. Hättest du z. B. capitol
nur einmal im Array falsch geschrieben, hätte es vorher keinen Fehler gegeben. Mit der Type-Annotation ist das aber der Fall:
const
states
:
State
[]
=
[
{
name
:
'Alabama'
,
capital
:
'Montgomery'
},
{
name
:
'Alaska'
,
capitol
:
'Juneau'
},
// ~~~~~~~ Did you mean to write 'capital'?
{
name
:
'Arizona'
,
capital
:
'Phoenix'
},
// ...
];
Das wird eine vertraute Dynamik, wenn du mit dem Typprüfer arbeitest: Je mehr Informationen du ihm gibst, desto mehr Probleme wird er finden können.
Im Hinblick auf das Venn-Diagramm können wir eine neue Gruppe von Programmen hinzufügen: TypeScript-Programme, die die Typprüfung bestehen (siehe Abbildung 1-2).
Wenn dir die Aussage "TypeScript ist eine Obermenge von JavaScript" falsch vorkommt, liegt das vielleicht daran, dass du an die dritte Gruppe von Programmen im Diagramm denkst. In der Praxis ist dies der wichtigste Satz für die tägliche Erfahrung mit TypeScript. Wenn du TypeScript verwendest, versuchst du in der Regel, dass dein Code alle Typprüfungen besteht.
Das Typsystem von TypeScript modelliert das Laufzeitverhalten von JavaScript. Das kann zu einigen Überraschungen führen, wenn du aus einer Sprache mit strengeren Laufzeitprüfungen kommst. Ein Beispiel:
const
x
=
2
+
'3'
;
// OK
// ^? const x: string
const
y
=
'2'
+
3
;
// OK
// ^? const y: string
Diese Anweisungen bestehen beide die Typprüfung, auch wenn sie fragwürdig sind und in vielen anderen Sprachen Laufzeitfehler produzieren. Aber dies bildet das Laufzeitverhalten von JavaScript genau ab, wo beide Ausdrücke die Zeichenkette "23"
ergeben.
TypeScript zieht jedoch irgendwo die Grenze. Der Typprüfer bemerkt Probleme in all diesen Anweisungen, auch wenn sie zur Laufzeit keine Ausnahmen auslösen:
const
a
=
null
+
7
;
// Evaluates to 7 in JS
// ~~~~ The value 'null' cannot be used here.
const
b
=
[]
+
12
;
// Evaluates to '12' in JS
// ~~~~~~~ Operator '+' cannot be applied to types ...
alert
(
'Hello'
,
'TypeScript'
);
// alerts "Hello"
// ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2
Das Leitprinzip des TypeScript-Typensystems ist, dass es das Laufzeitverhalten von JavaScript modellieren soll. Aber in all diesen Fällen hält TypeScript es für wahrscheinlicher, dass die seltsame Verwendung das Ergebnis eines Fehlers ist als die Absicht des Entwicklers, also geht es über die einfache Modellierung des Laufzeitverhaltens hinaus. Ein weiteres Beispiel dafür haben wir im Beispiel capital
/capitol
gesehen, bei dem das Programm zwar keine Fehlermeldung auslöste (es protokollierte undefined
), die Typüberprüfung aber trotzdem einen Fehler anzeigte.
Wie entscheidet TypeScript, wann es das Laufzeitverhalten von JavaScript modelliert und wann es darüber hinausgeht? Letztlich ist das eine Frage des Geschmacks. Wenn du dich für TypeScript entscheidest, vertraust du auf das Urteil des Teams, das die Software entwickelt. Wenn du gerne null
und 7
oder []
und 12
hinzufügst oder Funktionen mit überflüssigen Argumenten aufrufst, dann ist TypeScript vielleicht nichts für dich!
Wenn dein Programm den Typ prüft, könnte es trotzdem zur Laufzeit einen Fehler auslösen? Die Antwort ist "ja". Hier ist ein Beispiel:
const
names
=
[
'Alice'
,
'Bob'
];
console
.
log
(
names
[
2
].
toUpperCase
());
Wenn du das ausführst, wird es ausgelöst:
TypeError: Cannot read properties of undefined (reading 'toUpperCase')
TypeScript nahm an, dass der Array-Zugriff innerhalb der Grenzen liegen würde, aber das war nicht der Fall. Das Ergebnis war eine Ausnahme.
Unerwartete Fehler treten auch häufig auf, wenn du den Typ any
verwendest, was wir in Punkt 5 und in Kapitel 5 genauer besprechen werden.
Die Ursache für diese Ausnahmen ist, dass TypeScript den Typ eines Wertes (seinen statischen Typ) und seinen tatsächlichen Typ zur Laufzeit nicht richtig verstanden hat. Ein Typsystem, das die Genauigkeit seiner statischen Typen garantieren kann, gilt als solide. TypeScripts Typsystem ist weder solide, noch war es jemals dafür gedacht. In Artikel 48 werden weitere Möglichkeiten aufgezeigt, wie Unstimmigkeiten entstehen können.
Wenn dir die Sicherheit wichtig ist, solltest du dir andere Sprachen wie Reason, PureScript oder Dart ansehen. Diese Sprachen bieten zwar mehr Garantien für die Sicherheit zur Laufzeit, aber das hat seinen Preis: Es ist mehr Arbeit, ihre Typprüfer davon zu überzeugen, dass dein Code korrekt ist, und keine dieser Sprachen ist eine Obermenge von JavaScript, sodass die Migration komplizierter ist.
Dinge zum Erinnern
-
TypeScript ist eine Obermenge von JavaScript: Alle JavaScript-Programme sind syntaktisch gültige TypeScript-Programme, aber nicht alle TypeScript-Programme sind gültige JavaScript-Programme.
-
TypeScript fügt ein statisches Typsystem hinzu, das das Laufzeitverhalten von JavaScript modelliert und versucht, Code zu erkennen, der zur Laufzeit Ausnahmen auslöst.
-
Es ist möglich, dass der Code die Typprüfung besteht, aber trotzdem zur Laufzeit ausfällt.
-
TypeScript verbietet einige legale, aber fragwürdige JavaScript-Konstrukte wie den Aufruf von Funktionen mit der falschen Anzahl von Argumenten.
-
Typ-Annotationen teilen TypeScript deine Absicht mit und helfen ihm, korrekten und falschen Code zu unterscheiden.
Punkt 2: Wisse, welche TypeScript-Optionen du verwendest
Besteht dieser Code den Typprüfer?
function
add
(
a
,
b
)
{
return
a
+
b
;
}
add
(
10
,
null
);
Ohne zu wissen, welche Optionen du verwendest, ist es unmöglich, das zu sagen! Der TypeScript-Compiler verfügt über eine enorme Anzahl von Optionen, über hundert zum Zeitpunkt der Erstellung dieses Artikels.
Sie können über die Kommandozeile gesetzt werden:
$ tsc --noImplicitAny program.ts
oder über eine Konfigurationsdatei, tsconfig.json:
{
"compilerOptions"
:
{
"noImplicitAny"
:
true
}
}
Du solltest die Konfigurationsdatei bevorzugen. Sie stellt sicher, dass deine Mitarbeiter und Werkzeuge genau wissen, wie du TypeScript verwenden willst. Du kannst eine Konfigurationsdatei erstellen, indem du tsc
--init
.
Viele der TypeScript-Konfigurationseinstellungen steuern, wo die Sprache nach Quelldateien sucht und welche Art von Ausgabe sie erzeugt. Einige wenige steuern jedoch zentrale Aspekte der Sprache selbst. Dies sind Entscheidungen auf höchster Ebene, die die meisten Sprachen ihren Nutzern nicht überlassen. TypeScript kann sich wie eine ganz andere Sprache anfühlen, je nachdem, wie es konfiguriert ist. Um sie effektiv zu nutzen, solltest du die wichtigsten dieser Einstellungen verstehen: noImplicitAny
und strictNullChecks
.
noImplicitAny
noImplicitAny
steuert was TypeScript tut, wenn es den Typ einer Variablen nicht bestimmen kann. Dieser Code ist gültig, wenn noImplicitAny
ausgeschaltet ist:
function
add
(
a
,
b
)
{
return
a
+
b
;
}
Wenn du in deinem Editor mit der Maus über das Symbol add
fährst, siehst du, was TypeScript über den Typ der Funktion herausgefunden hat:
function
add
(
a
:
any
,
b
:
any
)
:
any
Die any
Typen deaktivieren die Typprüfung für Code mit diesen Parametern. any
ist ein nützliches Werkzeug, sollte aber mit Vorsicht verwendet werden. Mehr über any
findest du unter Punkt 5 und in Kapitel 5.
Diese werden implizite any
s genannt, weil du das Wort "any" nie geschrieben hast, aber trotzdem gefährliche any
Typen erhalten hast. Das wird ein Fehler, wenn du die Option noImplicitAny
setzt:
function
add
(
a
,
b
)
{
// ~ Parameter 'a' implicitly has an 'any' type
// ~ Parameter 'b' implicitly has an 'any' type
return
a
+
b
;
}
Diese Fehler können behoben werden, indem du explizit Typendeklarationen schreibst, entweder : any
oder einen spezifischeren Typ:
function
add
(
a
:
number
,
b
:
number
)
{
return
a
+
b
;
}
TypeScript ist am hilfreichsten, wenn es über Typinformationen verfügt. Deshalb solltest du, wann immer möglich, noImplicitAny
einstellen. Sobald du dich daran gewöhnt hast, dass alle Variablen Typen haben, fühlt sich TypeScript ohne noImplicitAny
fast wie eine andere Sprache an.
Bei neuen Projekten solltest du mit noImplicitAny
beginnen, so dass du die Typen schreibst, während du deinen Code schreibst. Das hilft TypeScript, Probleme zu erkennen, die Lesbarkeit deines Codes zu verbessern und deine Entwicklungserfahrung zu steigern (siehe Punkt 6).
Die Deaktivierung von noImplicitAny
ist nur sinnvoll, wenn du ein Projektvon JavaScript auf TypeScript umstellst (siehe Kapitel 10). Aber auch dann sollte dies nur ein vorübergehender Zustand sein und du solltest ihn so schnell wie möglich wieder aktivieren. TypeScript ohnenoImplicitAny
ist erstaunlich locker. In Artikel 83 erfährst du, wie das zu Problemen führen kann.
strictNullChecks
strictNullChecks
kontrolliert ob null
und undefined
in jedem Typ zulässige Werte sind.
Dieser Code ist gültig, wenn strictNullChecks
ausgeschaltet ist:
const
x
:
number
=
null
;
// OK, null is a valid number
löst aber einen Fehler aus, wenn du strictNullChecks
einschaltest:
const
x
:
number
=
null
;
// ~ Type 'null' is not assignable to type 'number'
Ein ähnlicher Fehler wäre aufgetreten, wenn du undefined
anstelle von null
verwendet hättest.
Wenn du beabsichtigst, null
zu erlauben, kannst du den Fehler beheben, indem du deine Absicht explizit machst:
const
x
:
number
|
null
=
null
;
Wenn du null
nicht zulassen willst, musst du herausfinden, woher das Geld kommt und entweder einen Scheck oder eine Erklärung hinzufügen:
const
statusEl
=
document
.
getElementById
(
'status'
);
statusEl
.
textContent
=
'Ready'
;
// ~~~~~ 'statusEl' is possibly 'null'.
if
(
statusEl
)
{
statusEl
.
textContent
=
'Ready'
;
// OK, null has been excluded
}
statusEl
!
.
textContent
=
'Ready'
;
// OK, we've asserted that el is non-null
Die Verwendung einer if
Anweisung auf diese Weise wird als "Verengung" oder "Verfeinerung" eines Typs bezeichnet, und dieses Muster wird in Punkt 22 untersucht. Das "!
" in der letzten Zeile wird als "Non-Null-Assertion" bezeichnet. Type Assertions haben ihren Platz in TypeScript, aber sie können auch zu Laufzeitausnahmen führen. In Punkt 9 wird erklärt, wann du eine Typ-Assertion verwenden solltest und wann nicht.
strictNullChecks
ist enorm hilfreich, um Fehler mit null
und undefined
Werten zu finden, aber es erhöht die Schwierigkeit, die Sprache zu verwenden. Wenn du ein neues Projekt beginnst und bereits TypeScript benutzt hast, solltest du strictNullChecks
einstellen.
Wenn du aber neu in der Sprache bist oder eine JavaScript-Codebasis migrierst, kannst du die Option auchweglassen. Du solltest auf jeden Fall noImplicitAny
setzen, bevor dustrictNullChecks
setzt.
Wenn du dich dafür entscheidest, ohne strictNullChecks
zu arbeiten, halte Ausschau nach dem gefürchteten Laufzeitfehler "undefined is not an object". Jeder dieser Fehler ist eine Erinnerung daran, dass du eine strengere Überprüfung aktivieren solltest. Das Ändern dieser Einstellung wird nur schwieriger, wenn dein Projekt wächst, also warte nicht zu lange mit der Aktivierung. Der meiste TypeScript-Code verwendet strictNullChecks
, und das ist schließlich der Punkt, an dem du sein willst.
Andere Optionen
Es gibt noch viele andere Einstellungen, die sich auf die Sprachsemantik auswirken (z.B. noImplicitThis
und strictFunctionTypes
), aber diese sind im Vergleich zu noImplicitAny
und strictNullChecks
unbedeutend. Um alle diese Prüfungen zu aktivieren, schalte die Einstellung strict
ein. TypeScript kann die meisten Fehler mit strict
abfangen, daher sollte dies dein Ziel sein.
Wenn du ein Projekt mit tsc --init
erstellst, bist du standardmäßig im strict
Modus.
Es gibt auch ein paar "strengere als strenge" Optionen. Du kannst diese Optionen wählen, um TypeScript noch aggressiver bei der Fehlersuche in deinem Code zu machen. Eine dieser Optionen ist noUncheckedIndexedAccess
, die hilft, Fehler beim Zugriff auf Objekte und Arrays abzufangen. Dieser Code weist zum Beispiel unter --strict
keine Typfehler auf, löst aber zur Laufzeit eine Ausnahme aus:
const
tenses
=
[
'past'
,
'present'
,
'future'
];
tenses
[
3
].
toUpperCase
();
Wenn noUncheckedIndexedAccess
eingestellt ist, ist dies ein Fehler:
const
tenses
=
[
'past'
,
'present'
,
'future'
];
tenses
[
3
].
toUpperCase
();
// ~~~~~~ Object is possibly 'undefined'.
Dies ist jedoch kein kostenloses Mittagessen. Viele gültige Zugänge werden auch als möglicherweise undefined
gekennzeichnet:
tenses
[
0
].
toUpperCase
();
// ~~~~~~ Object is possibly 'undefined'.
Einige TypeScript-Projekte verwenden diese Einstellung, andere nicht. Du solltest dir zumindest bewusst sein, dass es sie gibt. Mehr über diese Einstellung erfährst du in Punkt 48.
Wisse, welche Optionen du verwendest! Wenn ein Kollege oder eine Kollegin ein TypeScript-Beispiel mit dir teilt und du die Fehler nicht reproduzieren kannst, vergewissere dich, dass deine Compiler-Optionen dieselben sind.
Dinge zum Erinnern
-
Der TypeScript-Compiler enthält mehrere Einstellungen, die zentrale Aspekte der Sprache betreffen.
-
Konfiguriere TypeScript mit tsconfig.json anstatt mit Kommandozeilenoptionen.
-
Aktiviere
noImplicitAny
, es sei denn, du stellst ein JavaScript-Projekt aufTypeScript um. -
Verwende
strictNullChecks
, um Laufzeitfehler im Stil von "undefined is not an object" zu vermeiden. -
Ziel ist es,
strict
die gründlichste Prüfung zu ermöglichen, die TypeScript bieten kann.
Punkt 3: Verstehen, dass die Codegenerierung unabhängig von Typen ist
Auf einer hohen Ebene macht tsc
(der TypeScript-Compiler) zwei Dinge:
-
Es wandelt TypeScript/JavaScript der nächsten Generation in eine ältere Version von JavaScript um, die in Browsern oder anderen Laufzeiten funktioniert ("Transpilierung").
-
Sie prüft deinen Code auf Typfehler.
Erstaunlich ist, dass diese beiden Verhaltensweisen völlig unabhängig voneinander sind. Anders ausgedrückt: Die Typen in deinem Code haben keinen Einfluss auf das JavaScript, das TypeScript ausgibt. Da es dieses JavaScript ist, das ausgeführt wird, bedeutet das, dass deine Typen keinen Einfluss darauf haben, wie dein Code läuft.
Das hat einige überraschende Auswirkungen und sollte deine Erwartungen darüber beeinflussen, was TypeScript für dich tun kann und was nicht.
Du kannst TypeScript-Typen nicht zur Laufzeit prüfen
Du könntest versucht sein, einen Code wie diesen zu schreiben:
interface
Square
{
width
:
number
;
}
interface
Rectangle
extends
Square
{
height
:
number
;
}
type
Shape
=
Square
|
Rectangle
;
function
calculateArea
(
shape
:
Shape
)
{
if
(
shape
instanceof
Rectangle
)
{
// ~~~~~~~~~ 'Rectangle' only refers to a type,
// but is being used as a value here
return
shape
.
height
*
shape
.
width
;
// ~~~~~~ Property 'height' does not exist on type 'Shape'
}
else
{
return
shape
.
width
*
shape
.
width
;
}
}
Die Prüfung von instanceof
erfolgt zur Laufzeit, aber Rectangle
ist ein Typ und kann daher das Laufzeitverhalten des Codes nicht beeinflussen. TypeScript-Typen sind "löschbar": Ein Teil der Kompilierung zu JavaScript besteht darin, alle interface
s, type
s und Typ-Annotationen aus deinem Code zu entfernen. Das ist am einfachsten zu erkennen, wenn du dir das JavaScript ansiehst, zu dem dieses Beispiel kompiliert wird:
function
calculateArea
(
shape
)
{
if
(
shape
instanceof
Rectangle
)
{
return
shape
.
height
*
shape
.
width
;
}
else
{
return
shape
.
width
*
shape
.
width
;
}
}
Hier wird Rectangle
vor der instanceof
Prüfung nicht erwähnt, daher das Problem.2 Um herauszufinden, um welchen Typ von Form es sich handelt, brauchst du eine Möglichkeit, den Typ zur Laufzeit zu rekonstruieren, d.h. eine Möglichkeit, die im generierten JavaScript und nicht nur im ursprünglichen TypeScript Sinn macht.
Es gibt mehrere Möglichkeiten, dies zu tun. Eine davon ist, auf das Vorhandensein einer height
Eigenschaft zu prüfen:
function
calculateArea
(
shape
:
Shape
)
{
if
(
'height'
in
shape
)
{
return
shape
.
width
*
shape
.
height
;
// ^? (parameter) shape: Rectangle
}
else
{
return
shape
.
width
*
shape
.
width
;
}
}
Das funktioniert, weil die Eigenschaftsprüfung nur Werte umfasst, die zur Laufzeit verfügbar sind, aber trotzdem erlaubt, dass der Typchecker den Typ von shape
auf Rectangle
verfeinert.
Eine andere Möglichkeit wäre, ein "Tag" einzuführen, um den Typ explizit so zu speichern, dass er zur Laufzeit verfügbar ist:
interface
Square
{
kind
:
'square'
;
width
:
number
;
}
interface
Rectangle
{
kind
:
'rectangle'
;
height
:
number
;
width
:
number
;
}
type
Shape
=
Square
|
Rectangle
;
function
calculateArea
(
shape
:
Shape
)
{
if
(
shape
.
kind
===
'rectangle'
)
{
return
shape
.
width
*
shape
.
height
;
// ^? (parameter) shape: Rectangle
}
else
{
return
shape
.
width
*
shape
.
width
;
// ^? (parameter) shape: Square
}
}
Hier fungiert die Eigenschaft kind
als "Tag", und wir sagen, dass der Typ Shape
eine "getaggte Vereinigung" ist. Manchmal wird sie auch als "diskriminierte Vereinigung" bezeichnet, in diesem Fall ist kind
die "Diskriminante". Die Begriffe sind austauschbar. Weil sie es so einfach machen, Typinformationen zur Laufzeit wiederherzustellen, sind "tagged/discriminated unions" in TypeScript allgegenwärtig.
Einige Konstrukte führen sowohl einen Typ (der zur Laufzeit nicht verfügbar ist) als auch einen Wert (der verfügbar ist) ein. Das Schlüsselwort class
ist eines davon. Eine andere Möglichkeit, den Fehler zu beheben, wäre, Square
und Rectangle
zu Klassen zu machen:
class
Square
{
width
:
number
;
constructor
(
width
:
number
)
{
this
.
width
=
width
;
}
}
class
Rectangle
extends
Square
{
height
:
number
;
constructor
(
width
:
number
,
height
:
number
)
{
super
(
width
);
this
.
height
=
height
;
}
}
type
Shape
=
Square
|
Rectangle
;
function
calculateArea
(
shape
:
Shape
)
{
if
(
shape
instanceof
Rectangle
)
{
return
shape
.
width
*
shape
.
height
;
// ^? (parameter) shape: Rectangle
}
else
{
return
shape
.
width
*
shape
.
width
;
// ^? (parameter) shape: Square
}
}
Das funktioniert, weil class Rectangle
sowohl einen Typ als auch einen Wert einführt, während interface
nur einen Typ einführt.
Das Rectangle
in type Shape = Square | Rectangle
bezieht sich auf den Typ, aber das Rectangle
in shape instanceof Rectangle
bezieht sich auf den Wert, in diesem Fall die Konstruktorfunktion. Diese Unterscheidung ist wichtig zu verstehen, kann aber ziemlich subtil sein. Punkt 8 zeigt dir, wie du unterscheiden kannst.
Code mit Tippfehlern kann Ausgaben erzeugen
Da die Codeausgabe unabhängig von der Typüberprüfung ist, folgt daraus, dass Code mit Typfehlern eine Ausgabe erzeugen kann!
$ cat test.ts let x = 'hello'; x = 1234; $ tsc test.ts test.ts:2:1 - error TS2322: Type '1234' is not assignable to type 'string' 2 x = 1234; ~ $ cat test.js var x = 'hello'; x = 1234;
Das kann ziemlich überraschend sein, wenn du aus einer Sprache wie C oder Java kommst, wo Typprüfung und Ausgabe Hand in Hand gehen. Du kannst dir alle TypeScript-Fehler ähnlich wie Warnungen in diesen Sprachen vorstellen: Es ist wahrscheinlich, dass sie auf ein Problem hinweisen und eine Untersuchung wert sind, aber sie werden den Build nicht stoppen.
Code-Emissionen bei Vorliegen von Fehlern sind in der Praxis hilfreich. Wenn du eine Webanwendung entwickelst, weißt du vielleicht, dass es Probleme mit einem bestimmten Teil der Anwendung gibt. Da TypeScript aber auch bei Fehlern Code erzeugt, kannst du die anderen Teile deiner Anwendung testen, bevor du sie reparierst.
Du solltest beim Übertragen des Codes möglichst keine Fehler machen, damit du dich nicht daran erinnern musst, was ein erwarteter oder unerwarteter Fehler ist. Wenn du die Ausgabe von Fehlern deaktivieren möchtest, kannst du die Option noEmitOnError
in tsconfig.json oder das Äquivalent in deinem Build-Tool verwenden.
Typoperationen können keine Laufzeitwerte beeinflussen
Angenommen, du hast einen Wert, der sowohl eine Zeichenkette als auch eine Zahl sein kann, und du möchtest ihn so normalisieren, dass er immer eine Zahl ist. Hier ist ein fehlgeleiteter Versuch, den der Typprüfer akzeptiert:
function
asNumber
(
val
:
number
|
string
)
:
number
{
return
val
as
number
;
}
Wenn du dir das generierte JavaScript ansiehst, wird klar, was diese Funktion wirklich tut:
function
asNumber
(
val
)
{
return
val
;
}
Es findet überhaupt keine Umwandlung statt. as number
ist eine Typ-Operation und kann daher das Laufzeitverhalten deines Codes nicht beeinflussen. Um den Wert zu normalisieren, musst du seinen Laufzeittyp überprüfen und die Konvertierung mithilfe von JavaScript-Konstrukten vornehmen:
function
asNumber
(
val
:
number
|
string
)
:
number
{
return
Number
(
val
);
}
"as number
" ist eine Typbehauptung, die manchmal ungenau als "Cast" bezeichnet wird. Mehr darüber, wann es sinnvoll ist, Typ-Assertions zu verwenden, findest du unter Punkt 9.
Laufzeittypen dürfen nicht mit deklarierten Typen identisch sein
Könnte diese Funktion jemals die endgültige console.log
treffen?
function
setLightSwitch
(
value
:
boolean
)
{
switch
(
value
)
{
case
true
:
turnLightOn
();
break
;
case
false
:
turnLightOff
();
break
;
default
:
console
.
log
(
`I'm afraid I can't do that.`
);
}
}
Normalerweise meldet TypeScript toten Code, aber es beschwert sich nicht darüber, auch nicht mit der Option strict
. Wie konntest du diesen Zweig treffen?
Das Wichtigste ist, dass du dich daran erinnerst, dass boolean
der deklarierte Typ ist. Da es ein TypeScript-Typ ist, verschwindet er zur Laufzeit. Im JavaScript-Code könnte ein Benutzer versehentlich einensetLightSwitch
mit einem Wert wie "ON"
aufrufen.
Es gibt auch Möglichkeiten, diesen Codepfad in reinem TypeScript auszulösen. Vielleicht wird die Funktion mit einem Wert aufgerufen, der von einem Netzwerkaufruf stammt:
interface
LightApiResponse
{
lightSwitchValue
:
boolean
;
}
async
function
setLight
()
{
const
response
=
await
fetch
(
'/light'
);
const
result
:
LightApiResponse
=
await
response
.
json
();
setLightSwitch
(
result
.
lightSwitchValue
);
}
Du hast angegeben, dass das Ergebnis der /light
Anfrage LightApiResponse
ist, aber nichts erzwingt dies. Wenn du die API falsch verstanden hast und lightSwitchValue
in Wirklichkeit ein string
ist, wird zur Laufzeit ein String an setLightSwitch
übergeben. Oder vielleicht hat sich die API geändert, nachdem du sie eingesetzt hast.
TypeScript kann ziemlich verwirrend sein, wenn deine Laufzeittypen nicht mit den deklarierten Typen übereinstimmen. Du solltest diese so genannten "unsound" Typen vermeiden, wann immer du kannst. Aber sei dir bewusst, dass es möglich ist, dass ein Wert einen anderen Laufzeittyp hat als den, den du deklariert hast. Unter findest du weitere Informationen über Soundness, siehe Punkt 48.
Du kannst eine Funktion, die auf TypeScript-Typen basiert, nicht überladen
Sprachen wie C++ ermöglichen es dir, mehrere Versionen einer Funktion zu definieren, die sich nur durch die Typen ihrer Parameter unterscheiden. Das nennt man "Funktionsüberladung". Da das Laufzeitverhalten deines Codes unabhängig von seinen TypeScript-Typen ist, ist dieses Konstrukt in TypeScript nicht möglich:
function
add
(
a
:
number
,
b
:
number
)
{
return
a
+
b
;
}
// ~~~ Duplicate function implementation
function
add
(
a
:
string
,
b
:
string
)
{
return
a
+
b
;
}
// ~~~ Duplicate function implementation
TypeScript bietet zwar die Möglichkeit, Funktionen zu überladen, aber das geschieht ausschließlich auf der Typebene. Du kannst mehrere Typsignaturen für eine Funktion angeben, aber nur eine einzige Implementierung:
function
add
(
a
:
number
,
b
:
number
)
:
number
;
function
add
(
a
:
string
,
b
:
string
)
:
string
;
function
add
(
a
:
any
,
b
:
any
)
{
return
a
+
b
;
}
const
three
=
add
(
1
,
2
);
// ^? const three: number
const
twelve
=
add
(
'1'
,
'2'
);
// ^? const twelve: string
Die ersten beiden Signaturen von add
liefern nur Typinformationen. Wenn TypeScript eine JavaScript-Ausgabe erzeugt, werden sie entfernt und es bleibt nur die Implementierung übrig. Die any
Parameter in der Implementierung sind nicht besonders gut. Wie du damit umgehst, erfährst du in Artikel 52, in dem es auch um einige Feinheiten geht, die bei TypeScript-Funktionsüberladungen zu beachten sind.
TypeScript-Typen haben keinen Einfluss auf die Laufzeitleistung
Da Typen und Typoperationen beim Generieren von JavaScript gelöscht werden, können sie sich nicht auf die Laufzeitleistung auswirken. Die statischen Typen von TypeScript sind wirklich kostenlos. Wenn das nächste Mal jemand Laufzeit-Overhead als Grund anführt, TypeScript nicht zu verwenden, wirst du genau wissen, wie gut er diese Behauptung getestet hat!
Dabei gibt es zwei Vorbehalte:
-
Während es keinen Laufzeit-Overhead gibt, verursacht der TypeScript-Compiler einen Overhead bei der Build-Zeit. Das TypeScript-Team nimmt die Leistung des Compilers sehr ernst, und die Kompilierung ist in der Regel recht schnell, vor allem bei inkrementellen Builds. Wenn der Mehraufwand zu groß wird, kann dein Build-Tool die Option "Transpile only" anbieten, um die Typüberprüfung zu überspringen. Mehr über die Leistung des Compilers erfährst du in Artikel 78.
-
Der Code, den TypeScript ausgibt, um ältere Laufzeiten zu unterstützen , kann im Vergleich zu nativen Implementierungen einen Leistungszuschlag bedeuten. Wenn du z. B. Generatorfunktionen verwendest und auf ES5 abzielst, das vor den Generatoren liegt, gibt
tsc
Hilfscode aus, damit alles funktioniert. Dadurch entsteht ein gewisser Overhead im Vergleich zu einer nativen Implementierung von Generatoren. Das ist bei jedem JavaScript-"Transpiler" der Fall, nicht nur bei TypeScript. Unabhängig davon hat dies mit dem Emit-Ziel und der Sprachebene zu tun und ist immer noch unabhängig von .
Dinge zum Erinnern
-
Die Codegenerierung ist unabhängig vom Typsystem. Das bedeutet, dass TypeScript-Typen keinen Einfluss auf das Laufzeitverhalten deines Codes haben.
-
Es ist möglich, dass ein Programm mit Tippfehlern Code erzeugt ("kompiliert").
-
TypeScript-Typen sind zur Laufzeit nicht verfügbar. Um einen Typ zur Laufzeit abzufragen, brauchst du eine Möglichkeit, ihn zu rekonstruieren. Tagged Unions und Property Checking sind gängige Methoden, um dies zu tun.
-
Einige Konstrukte, wie z.B.
class
, führen sowohl einen TypeScript-Typ als auch einen Wert ein, der zur Laufzeit verfügbar ist. -
Da sie bei der Kompilierung gelöscht werden, können TypeScript-Typen die Laufzeitleistung von deinem Code nicht beeinflussen.
Punkt 4: Mach dich mit der Strukturtypisierung vertraut
JavaScript fördert das "Duck Typing": Wenn du einer Funktion einen Wert mit den richtigen Eigenschaften übergibst, ist es ihr egal, wie du den Wert erstellt hast. Sie wird ihn einfach verwenden. (Dieser Begriff bezieht sich auf das Sprichwort: "Wenn es wie eine Ente läuft und wie eine Ente spricht, dann ist es wahrscheinlich eine Ente").
TypeScript modelliert dieses Verhalten durch ein sogenanntes strukturelles Typsystem. Das kann manchmal zu überraschenden Ergebnissen führen, weil der Typprüfer ein anderes Verständnis von einem Typ hat, als du es dir vorgestellt hast. Ein gutes Verständnis der strukturellen Typisierung hilft dir, Fehler und Nicht-Fehler zu erkennen und robusteren Code zu schreiben.
Angenommen, du arbeitest an einer Physikbibliothek und hast einen 2D-Vektortyp:
interface
Vector2D
{
x
:
number
;
y
:
number
;
}
Du schreibst eine Funktion, um ihre Länge zu berechnen:
function
calculateLength
(
v
:
Vector2D
)
{
return
Math
.
sqrt
(
v
.
x
**
2
+
v
.
y
**
2
);
}
Jetzt führst du den Begriff des benannten Vektors ein:
interface
NamedVector
{
name
:
string
;
x
:
number
;
y
:
number
;
}
Die Funktion calculateLength
funktioniert mit NamedVector
s, weil sie x
und y
Eigenschaften haben, die number
s sind. TypeScript ist schlau genug, das herauszufinden:
const
v
:
NamedVector
=
{
x
:
3
,
y
:
4
,
name
:
'Pythagoras'
};
calculateLength
(
v
);
// OK, result is 5
Interessant ist, dass du die Beziehung zwischen Vector2D
und NamedVector
nie erklärt hast. Und du musstest auch nicht eine alternative Implementierung von calculateLength
für NamedVector
s schreiben. Das Typsystem von TypeScript modelliert das Laufzeitverhalten von JavaScript(Punkt 1). Es ermöglichte den Aufruf von calculateLength
mit einemNamedVector
aufgerufen werden, weil seine Struktur mit Vector2D
kompatibel war. Daher kommt auch der Begriff "strukturelle Typisierung".
Aber das kann auch zu Problemen führen. Angenommen, du fügst einen 3D-Vektortyp hinzu:
interface
Vector3D
{
x
:
number
;
y
:
number
;
z
:
number
;
}
und schreibe eine Funktion, um sie zu normalisieren (ihre Länge auf 1 zu setzen):
function
normalize
(
v
:
Vector3D
)
{
const
length
=
calculateLength
(
v
);
return
{
x
:
v
.
x
/
length
,
y
:
v
.
y
/
length
,
z
:
v
.
z
/
length
,
};
}
Wenn du diese Funktion aufrufst, wirst du wahrscheinlich einen Vektor mit einer Länge größer als 1 erhalten:
> normalize({x: 3, y: 4, z: 5}) { x: 0.6, y: 0.8, z: 1 }
Dieser Vektor hat eine Länge von etwa 1,4 und nicht 1. Was ist also schief gelaufen und warum hat TypeScript den Fehler nicht erkannt?
Der Fehler ist, dass calculateLength
mit 2D-Vektoren arbeitet, normalize
aber mit 3D-Vektoren. Daher wird die Komponente z
bei der Normalisierung ignoriert.
Was vielleicht noch überraschender ist, ist, dass der Typprüfer dieses Problem nicht erkennt. Warum darfst du calculateLength
mit einem 3D-Vektor aufrufen, obwohl die Typdeklaration besagt, dass sie 2D-Vektoren akzeptiert?
Was bei benannten Vektoren so gut funktioniert hat, geht hier nach hinten los. Der Aufruf von calculateLength
mit einem {x, y, z}
Objekt führt nicht zu einem Fehler. Der Typprüfer beschwert sich also auch nicht, und dieses Verhalten hat zu einem Fehler geführt.
(Wenn du möchtest, dass dies ein Fehler ist, hast du einige Möglichkeiten. In Punkt 63 wird ein Trick vorgestellt, mit dem du die Eigenschaft z
gezielt verbieten kannst, und in Punkt 64 wird gezeigt, wie du "Marken" verwenden kannst, um diese Art der strukturellen Typisierung ganz zu verhindern).
Wenn du Funktionen schreibst, kannst du dir leicht vorstellen, dass sie mit Argumenten aufgerufen werden, die die von dir deklarierten Eigenschaften haben und keine anderen. Das ist bekannt als ein "geschlossener", "versiegelter" oder "präziser" Typ und kann im Typsystem von TypeScript nicht ausgedrückt werden. Ob es dir gefällt oder nicht, deine Typen sind "offen".
Das kann manchmal zu Überraschungen führen:
function
calculateLengthL1
(
v
:
Vector3D
)
{
let
length
=
0
;
for
(
const
axis
of
Object
.
keys
(
v
))
{
const
coord
=
v
[
axis
];
// ~~~~~~~ Element implicitly has an 'any' type because ...
// 'string' can't be used to index type 'Vector3D'
length
+=
Math
.
abs
(
coord
);
}
return
length
;
}
Warum ist das ein Fehler? Da axis
einer der Schlüssel von v
ist, das ein Vector3D
ist, sollte es entweder "x"
, "y"
oder "z"
sein. Und laut der Deklaration von Vector3D
sind das alles number
s, sollte der Typ von coord
also nicht number
sein?
Ist dieser Fehler ein falsches Positiv? Nein! TypeScript hat Recht, wenn es sich beschwert. Die Logik im vorherigen Absatz geht davon aus, dass Vector3D
versiegelt ist und keine anderen Eigenschaften hat. Das könnte aber der Fall sein:
const
vec3D
=
{
x
:
3
,
y
:
4
,
z
:
1
,
address
:
'123 Broadway'
};
calculateLengthL1
(
vec3D
);
// OK, returns NaN
Da v
jede beliebige Eigenschaft haben kann, ist der Typ von axis
string
. TypeScript hat keinen Grund zu glauben, dass v[axis]
eine Zahl ist, denn wie du gerade gesehen hast, könnte sie es nicht sein. (Die Variable vec3D
vermeidet hier eine übermäßige Eigenschaftsprüfung, die Gegenstand von Punkt 11 ist).
Das Iterieren über Objekte kann knifflig sein, wenn man sie richtig tippen will. Wir werden in Punkt 60 auf dieses Thema zurückkommen, aber in diesem Fall wäre eine Implementierung ohne Schleifen besser:
function
calculateLengthL1
(
v
:
Vector3D
)
{
return
Math
.
abs
(
v
.
x
)
+
Math
.
abs
(
v
.
y
)
+
Math
.
abs
(
v
.
z
);
}
Strukturelle Typisierung kann auch bei class
es zu Überraschungen führen, die strukturell für Zuordenbarkeit verglichen werden:
class
SmallNumContainer
{
num
:
number
;
constructor
(
num
:
number
)
{
if
(
num
<
0
||
num
>=
10
)
{
throw
new
Error
(
`You gave me
${
num
}
but I want something 0-9.`
)
}
this
.
num
=
num
;
}
}
const
a
=
new
SmallNumContainer
(
5
);
const
b
:
SmallNumContainer
=
{
num
:
2024
};
// OK!
Warum ist b
der SmallNumContainer
zuzuordnen? Es hat eine num
Eigenschaft, die eine number
ist. Die Strukturen stimmen also überein. Das kann zu Problemen führen, wenn du eine Funktion schreibst, die davon ausgeht, dass die Validierungslogik im Konstruktor von SmallNumContainer
ausgeführt wurde. Bei Klassen mit mehreren Eigenschaften und Methoden ist es weniger wahrscheinlich, dass dies zufällig passiert, aber es ist ganz anders als in Sprachen wie C++ oder Java, wo die Deklaration eines Parameters vom Typ SmallNumContainer
garantiert, dass er entweder SmallNumContainer
oder eine Unterklasse davon ist und somit die Validierungslogik im Konstruktor ausgeführt wurde.
Strukturelle Typisierung ist von Vorteil, wenn du Tests schreibst. Angenommen, du hast eine Funktion, die eine Abfrage in einer Datenbank ausführt und die Ergebnisse verarbeitet:
interface
Author
{
first
:
string
;
last
:
string
;
}
function
getAuthors
(
database
:
PostgresDB
)
:
Author
[]
{
const
authorRows
=
database
.
runQuery
(
`SELECT first, last FROM authors`
);
return
authorRows
.
map
(
row
=>
({
first
:
row
[
0
],
last
:
row
[
1
]}));
}
Um dies zu testen, könntest du eine Attrappe PostgresDB
erstellen. Einfacher ist es jedoch, die strukturelle Typisierung zu verwenden und eine engere Schnittstelle zu definieren:
interface
DB
{
runQuery
:
(
sql
:
string
)
=>
any
[];
}
function
getAuthors
(
database
:
DB
)
:
Author
[]
{
const
authorRows
=
database
.
runQuery
(
`SELECT first, last FROM authors`
);
return
authorRows
.
map
(
row
=>
({
first
:
row
[
0
],
last
:
row
[
1
]}));
}
Du kannst getAuthors
trotzdem eine PostgresDB
in der Produktion übergeben, da sie eine runQuery
Methode hat. Wegen der strukturellen Typisierung muss PostgresDB
nicht sagen, dass es DB
implementiert. TypeScript findet heraus, dass sie es tut.
Wenn du deine Tests schreibst, kannst du stattdessen ein einfacheres Objekt übergeben:
test
(
'getAuthors'
,
()
=>
{
const
authors
=
getAuthors
({
runQuery
(
sql
:
string
)
{
return
[[
'Toni'
,
'Morrison'
],
[
'Maya'
,
'Angelou'
]];
}
});
expect
(
authors
).
toEqual
([
{
first
:
'Toni'
,
last
:
'Morrison'
},
{
first
:
'Maya'
,
last
:
'Angelou'
}
]);
});
TypeScript überprüft, ob unser Test DB
mit der Schnittstelle übereinstimmt. Und deine Tests müssen nichts über deine Produktionsdatenbank wissen: keine Mocking-Bibliotheken notwendig! Durch die Einführung einer Abstraktion (DB
) haben wir unsere Logik (und die Tests) von den Details einer bestimmten Implementierung (PostgresDB
) befreit.
Ein weiterer Vorteil der strukturellen Typisierung ist, dass sie Abhängigkeiten zwischen Bibliotheken sauber trennen kann. Mehr dazu erfährst du unter Punkt 70.
Dinge zum Erinnern
-
Verstehe, dass JavaScript duck typed ist und TypeScript strukturelle Typisierung verwendet, um dies zu modellieren: Werte, die deinen Interfaces zugewiesen werden, können Eigenschaften haben, die über die explizit in deinen Typdeklarationen aufgeführten hinausgehen. Typen sind nicht "versiegelt".
-
Sei dir bewusst, dass Klassen auch strukturellen Typisierungsregeln folgen. Es kann sein, dass du keine Instanz der Klasse hast, die du erwartest!
-
Verwende die strukturelle Typisierung, um Unit-Tests zu erleichtern.
Punkt 5: Beschränke die Verwendung eines beliebigen Typs
Das Typsystem von TypeScript ist schrittweise und optional: schrittweise, weil du deinem Code nach und nach Typen hinzufügen kannst (mit noImplicitAny
), und optional, weil du die Typüberprüfung jederzeit deaktivieren kannst. Der Schlüssel zu diesen Funktionen ist der any
Typ:
let
ageInYears
:
number
;
ageInYears
=
'12'
;
// ~~~~~~~ Type 'string' is not assignable to type 'number'.
ageInYears
=
'12'
as
any
;
// OK
Der Typprüfer hat Recht, wenn er sich hier beschwert, aber du kannst ihn zum Schweigen bringen, indem du as any
eingibst. Wenn du anfängst, TypeScript zu benutzen, ist es verlockend, any
types und type assertions (as any
) zu verwenden, wenn du einen Fehler nicht verstehst, denkst, dass der Typprüfer falsch liegt, oder dir einfach nicht die Zeit nehmen willst, Typendeklarationen zu schreiben.
In manchen Fällen mag das in Ordnung sein, aber sei dir bewusst, dass die Verwendung von any
viele der Vorteile von TypeScript eliminiert. Du solltest zumindest die Gefahren verstehen, bevor du es benutzt.
Es gibt keine Typensicherheit bei allen Typen
Im vorangegangenen Beispiel besagt die Typdeklaration, dass ageInYears
ein number
ist. Aber any
erlaubt es dir, ihm ein string
zuzuweisen. Der Typprüfer wird glauben, dass es sich um ein number
handelt (schließlich hast du das gesagt), und das Chaos bleibt unentdeckt:
ageInYears
+=
1
;
// OK; at runtime, ageInYears is now "121"
jeder lässt dich Verträge brechen
Wenn du eine Funktion schreibst, legst du einen Vertrag fest: Wenn der Aufrufer dir eine bestimmte Art von Eingabe gibt, wirst du eine bestimmte Art von Ausgabe produzieren. Aber mit any
kannst du diese Verträge brechen:
function
calculateAge
(
birthDate
:
Date
)
:
number
{
// ...
}
let
birthDate
:
any
=
'1990-01-19'
;
calculateAge
(
birthDate
);
// OK
Der Parameter für das Geburtsdatum sollte ein Date
sein, nicht ein string
. Der Typ any
hat dich den Vertrag von calculateAge
brechen lassen. Das kann besonders problematisch sein, weil JavaScript oft bereit ist, implizit zwischen Typen zu konvertieren. Ein string
funktioniert manchmal dort, wo ein number
erwartet wird, nur um unter anderen Umständen zu scheitern.
Es gibt keine Sprachdienste für alle Arten
Wenn eine -Variable einen Typ hat, der nichtany
ist, können die TypeScript-Sprachdienste intelligente Autovervollständigung und kontextbezogene Dokumentationbereitstellen (wie in Abbildung 1-3 gezeigt).
Aber bei Symbolen mit einem any
Typ bist du auf dich allein gestellt(Abbildung 1-4).
Das Umbenennen ist ein weiterer solcher Dienst. Wenn du einen Person
Typ und Funktionen zum Formatieren des Namens einer Person hast:
interface
Person
{
first
:
string
;
last
:
string
;
}
const
formatName
=
(
p
:
Person
)
=>
`
${
p
.
first
}
${
p
.
last
}
`
;
const
formatNameAny
=
(
p
:
any
)
=>
`
${
p
.
first
}
${
p
.
last
}
`
;
dann kannst du in deinem Editor first
auswählen, "Symbol umbenennen" wählen und es in firstName
ändern (siehe Abbildungen 1-5 und 1-6).
Dadurch wird die Funktion formatName
geändert, nicht aber die Version any
:
interface
Person
{
firstName
:
string
;
last
:
string
;
}
const
formatName
=
(
p
:
Person
)
=>
`
${
p
.
firstName
}
${
p
.
last
}
`
;
const
formatNameAny
=
(
p
:
any
)
=>
`
${
p
.
first
}
${
p
.
last
}
`
;
Einer der Slogans von TypeScript ist "JavaScript, das skaliert". Ein wichtiger Bestandteil von "Skalierung" sind die Sprachdienste, die ein wesentlicher Bestandteil der TypeScript-Erfahrung sind (siehe Punkt 6). Wenn du sie verlierst, bedeutet das einen Produktivitätsverlust, nicht nur für dich, sondern auch für alle anderen, die mit deinem Code arbeiten.
alle Arten von Bugs maskieren, wenn du Code refaktorisierst
Angenommen, du baust eine Webanwendung, in der die Benutzer eine Art von Artikel auswählen können. Eine deiner Komponenten könnte einen onSelectItem
Callback haben. Einen Typ für einen Artikel zu schreiben, scheint dir zu mühsam zu sein, also benutzt du einfach any
als Ersatz:
interface
ComponentProps
{
onSelectItem
:
(
item
:
any
)
=>
void
;
}
Hier ist der Code, der diese Komponente verwaltet:
function
renderSelector
(
props
:
ComponentProps
)
{
/* ... */
}
let
selectedId
:
number
=
0
;
function
handleSelectItem
(
item
:
any
)
{
selectedId
=
item
.
id
;
}
renderSelector
({
onSelectItem
:
handleSelectItem
});
Später überarbeitest du den Selektor so, dass es schwieriger wird, das ganze item
Objekt an onSelectItem
zu übergeben. Aber das ist nicht weiter schlimm, da du nur die ID brauchst. Du änderst die Signatur in ComponentProps
:
interface
ComponentProps
{
onSelectItem
:
(
id
:
number
)
=>
void
;
}
Du aktualisierst die Komponente und alles besteht die Typprüfung. Sieg!
...oder doch nicht? handleSelectItem
nimmt einen any
Parameter an, ist also mit einem Item genauso zufrieden wie mit einer ID. Er erzeugt eine Laufzeitausnahme, obwohl er die Typprüfung bestanden hat. Hättest du einen spezifischeren Typ verwendet, wäre dies von der Typüberprüfung erkannt worden.
jeder versteckt dein Schriftdesign
Die Typdefinition für komplexe Objekte, wie den Status deiner Anwendung, kann ziemlich lang werden. Anstatt Typen für die Dutzenden von Eigenschaften im Status deiner Anwendung zu schreiben, könntest du versucht sein, einfach einen any
Typ zu verwenden und damit fertig zu sein.
Das ist aus all den Gründen, die in diesem Artikel aufgeführt sind, problematisch. Aber es ist auch deshalb problematisch, weil es das Design deines Status verbirgt. Wie in Kapitel 4 erklärt, ist ein gutes Typendesign wichtig, um sauberen, korrekten und verständlichen Code zu schreiben. Mit einem any
Typ ist dein Typdesign implizit. Das macht es schwer zu erkennen, ob das Design gut ist oder ob es überhaupt ein Design gibt. Wenn du einen Kollegen bittest, eine Änderung zu überprüfen, muss er rekonstruieren, ob und wie du den Zustand der Anwendung geändert hast. Besser ist es, wenn du es für alle sichtbar aufschreibst.
jeder untergräbt das Vertrauen in das Schriftsystem
Jedes Mal, wenn du einen Fehler machst und der Typprüfer ihn findet, stärkt das dein Vertrauen in das Typsystem. Aber wenn du zur Laufzeit einen Typfehler siehst, den TypeScript nicht erkannt hat, bekommt dieses Vertrauen einen Dämpfer. Wenn du TypeScript in einem größeren Team einführst, könnten sich deine Kollegen fragen, ob TypeScript die Mühe wert ist. any
Typen sind oft die Ursache für diese nicht abgefangenen Fehler.
TypeScript soll dir das Leben leichter machen, aber TypeScript mit vielen any
Typen kann schwieriger zu handhaben sein als untypisiertes JavaScript, weil du Typfehler beheben und gleichzeitig die echten Typen im Kopf behalten musst. Wenn deine Typen mit der Realität übereinstimmen, bist du von der Last befreit, die Typinformationen im Kopf behalten zu müssen. TypeScript behält sie für dich im Auge.
Für die Fälle, in denen du any
verwenden musst, gibt es bessere und schlechtere Möglichkeiten, es zu tun. Mehr darüber, wie du die Nachteile von any
einschränken kannst, erfährst du in Kapitel 5.
Dinge zum Erinnern
-
Mit dem Typ
any
von TypeScript kannst du die meisten Formen der Typenprüfung für ein Symbol deaktivieren. -
Der
any
Typ eliminiert die Typsicherheit, lässt dich Verträge brechen, schadet der Erfahrung der Entwickler, macht das Refactoring fehleranfällig, verbirgt dein Typdesign und untergräbt das Vertrauen in das Typsystem.
1 Vielleicht stößt du auf .tsx, .jsx, .mts, .mjs und ein paar andere Erweiterungen. Das sind alles TypeScript- und JavaScript-Dateien.
2 Der beste Weg, um ein Gefühl dafür zu bekommen, ist der TypeScript-Spielplatz, der dein TypeScript und das resultierende JavaScript nebeneinander zeigt.
Get Effektives TypeScript, 2. Auflage now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.