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