undefined|TDieses Kapitel erklärt die Grundlagen von TypeScript.
Nachdem Sie dieses Kapitel gelesen haben, sollten Sie in der Lage sein, den folgenden TypeScript-Code zu verstehen
interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U
): U;
// ···
}Sie denken vielleicht, das sei kryptisch. Und ich stimme Ihnen zu! Aber (wie ich zu beweisen hoffe) ist diese Syntax relativ leicht zu lernen. Und sobald Sie sie verstanden haben, liefert sie Ihnen sofortige, präzise und umfassende Zusammenfassungen, wie Code funktioniert – ohne lange Beschreibungen auf Englisch lesen zu müssen.
Es gibt viele Möglichkeiten, wie der TypeScript-Compiler konfiguriert werden kann. Eine wichtige Gruppe von Optionen steuert, wie gründlich der Compiler TypeScript-Code prüft. Die maximale Einstellung wird über --strict aktiviert und ich empfehle, diese immer zu verwenden. Sie macht Programme etwas schwieriger zu schreiben, aber wir erhalten auch die vollen Vorteile der statischen Typprüfung.
Das ist alles, was Sie vorerst über
--strict wissen müssen
Lesen Sie weiter, wenn Sie mehr Details erfahren möchten.
Wenn --strict auf true gesetzt wird, werden alle folgenden Optionen auf true gesetzt
--noImplicitAny: Wenn TypeScript keinen Typ ableiten kann, müssen wir ihn angeben. Dies gilt hauptsächlich für Parameter von Funktionen und Methoden: Mit dieser Einstellung müssen wir sie annotieren.--noImplicitThis: Beschweren Sie sich, wenn der Typ von this unklar ist.--alwaysStrict: Verwenden Sie den Strict-Modus von JavaScript, wann immer möglich.--strictNullChecks: null ist kein Teil eines Typs (außer seines eigenen Typs, null) und muss explizit erwähnt werden, wenn es ein akzeptabler Wert ist.--strictFunctionTypes: ermöglicht stärkere Prüfungen für Funktionstypen.--strictPropertyInitialization: Eigenschaften in Klassendefinitionen müssen initialisiert werden, es sei denn, sie können den Wert undefined haben.Weitere Compiler-Optionen werden wir später in diesem Buch sehen, wenn wir mit TypeScript npm-Pakete und Web-Apps erstellen. Das TypeScript-Handbuch enthält umfassende Dokumentation dazu.
In diesem Kapitel ist ein Typ einfach eine Menge von Werten. Die JavaScript-Sprache (nicht TypeScript!) hat nur acht Typen
undefinednullfalse und trueAll diese Typen sind dynamisch: Wir können sie zur Laufzeit verwenden.
TypeScript bringt eine zusätzliche Ebene zu JavaScript: statische Typen. Diese existieren nur beim Kompilieren oder Typ-Checken von Quellcode. Jede Speicherstelle (Variable, Eigenschaft usw.) hat einen statischen Typ, der ihre dynamischen Werte vorhersagt. Die Typprüfung stellt sicher, dass diese Vorhersagen eintreffen.
Und es gibt vieles, das statisch (ohne Ausführung des Codes) geprüft werden kann. Wenn zum Beispiel der Parameter num einer Funktion toString(num) den statischen Typ number hat, dann ist der Funktionsaufruf toString('abc') illegal, weil das Argument 'abc' den falschen statischen Typ hat.
function toString(num: number): string {
return String(num);
}In der vorherigen Funktionsdeklaration gibt es zwei Typ-Annotationen
num: Doppelpunkt gefolgt von numbertoString(): Doppelpunkt gefolgt von stringSowohl number als auch string sind Typausdrücke, die die Typen von Speicherstellen angeben.
Oft kann TypeScript einen statischen Typ inferieren, wenn keine Typ-Annotation vorhanden ist. Wenn wir zum Beispiel den Rückgabetyp von toString() weglassen, schlussfolgert TypeScript, dass er string ist.
// %inferred-type: (num: number) => string
function toString(num: number) {
return String(num);
}Typ-Inferenz ist kein Rätselraten: Sie folgt klaren Regeln (ähnlich der Arithmetik), um Typen abzuleiten, wo sie nicht explizit angegeben wurden. In diesem Fall wendet die return-Anweisung eine Funktion String() an, die beliebige Werte auf Zeichenketten abbildet, auf einen Wert num vom Typ number und gibt das Ergebnis zurück. Deshalb ist der abgeleitete Rückgabetyp string.
Wenn der Typ einer Stelle weder explizit angegeben noch ableitbar ist, verwendet TypeScript dafür den Typ any. Dies ist der Typ aller Werte und ein Platzhalter, insofern wir alles tun können, wenn ein Wert diesen Typ hat.
Mit --strict ist any nur erlaubt, wenn wir es explizit verwenden. Mit anderen Worten: Jede Stelle muss einen expliziten oder abgeleiteten statischen Typ haben. Im folgenden Beispiel hat der Parameter num keinen von beiden und wir erhalten einen Kompilierungsfehler.
// @ts-expect-error: Parameter 'num' implicitly has an 'any' type. (7006)
function toString(num) {
return String(num);
}Die Typausdrücke nach den Doppelpunkten von Typ-Annotationen reichen von einfach bis komplex und werden wie folgt erstellt.
Grundlegende Typen sind gültige Typausdrücke
undefined, nullboolean, number, bigint, stringsymbolobject.Array (technisch kein Typ in JavaScript)any (der Typ aller Werte)Es gibt viele Möglichkeiten, grundlegende Typen zu kombinieren, um neue, zusammengesetzte Typen zu erzeugen. Zum Beispiel über Typoperatoren, die Typen ähnlich kombinieren, wie die Mengenoperatoren Vereinigung (∪) und Schnittmenge (∩) Mengen kombinieren. Wie das geht, sehen wir bald.
TypeScript hat zwei Sprachebenen
Diese beiden Ebenen sehen wir in der Syntax
const undef: undefined = undefined;Auf der dynamischen Ebene verwenden wir JavaScript, um eine Variable undef zu deklarieren und sie mit dem Wert undefined zu initialisieren.
Auf der statischen Ebene verwenden wir TypeScript, um anzugeben, dass die Variable undef den statischen Typ undefined hat.
Beachten Sie, dass dieselbe Syntax, undefined, je nachdem, ob sie auf der dynamischen oder statischen Ebene verwendet wird, unterschiedliche Bedeutungen hat.
Versuchen Sie, ein Bewusstsein für die beiden Sprachebenen zu entwickeln
Das hilft erheblich, TypeScript zu verstehen.
Mit type können wir einen neuen Namen (einen Alias) für einen vorhandenen Typ erstellen
type Age = number;
const age: Age = 82;Arrays spielen zwei Rollen in JavaScript (entweder eine oder beide)
Es gibt zwei Möglichkeiten, die Tatsache auszudrücken, dass das Array arr als Liste verwendet wird, deren Elemente alle Zahlen sind
let arr1: number[] = [];
let arr2: Array<number> = [];Normalerweise kann TypeScript den Typ einer Variablen ableiten, wenn eine Zuweisung vorliegt. In diesem Fall müssen wir ihm tatsächlich helfen, da es bei einem leeren Array den Elementtyp nicht bestimmen kann.
Wir werden später auf die Winkelklammer-Notation (Array<number>) zurückkommen.
Wenn wir einen zweidimensionalen Punkt in einem Array speichern, dann verwenden wir dieses Array als Tupel. Das sieht wie folgt aus
let point: [number, number] = [7, 5];Die Typ-Annotation ist für Arrays-als-Tupel erforderlich, da TypeScript für Array-Literale Listentypen und nicht Tupeltypen ableitet.
// %inferred-type: number[]
let point = [7, 5];Ein weiteres Beispiel für Tupel ist das Ergebnis von Object.entries(obj): ein Array mit einem [key, value]-Paar für jede Eigenschaft von obj.
// %inferred-type: [string, number][]
const entries = Object.entries({ a: 1, b: 2 });
assert.deepEqual(
entries,
[[ 'a', 1 ], [ 'b', 2 ]]);Der abgeleitete Typ ist ein Array von Tupeln.
Dies ist ein Beispiel für einen Funktionstyp
(num: number) => stringDieser Typ umfasst jede Funktion, die einen einzelnen Parameter vom Typ number akzeptiert und einen string zurückgibt. Verwenden wir diesen Typ in einer Typ-Annotation
const toString: (num: number) => string = // (A)
(num: number) => String(num); // (B)Normalerweise müssen wir Parametertypen für Funktionen angeben. Aber in diesem Fall kann der Typ von num aus dem Funktionstyp in Zeile A abgeleitet werden und wir können ihn weglassen.
const toString: (num: number) => string =
(num) => String(num);Wenn wir die Typ-Annotation für toString weglassen, leitet TypeScript einen Typ aus dem Pfeil-Funktion ab.
// %inferred-type: (num: number) => string
const toString = (num: number) => String(num);Dieses Mal muss num eine Typ-Annotation haben.
Das folgende Beispiel ist komplizierter
function stringify123(callback: (num: number) => string) {
return callback(123);
}Wir verwenden einen Funktionstyp, um den Parameter callback von stringify123() zu beschreiben. Aufgrund dieser Typ-Annotation lehnt TypeScript den folgenden Funktionsaufruf ab.
// @ts-expect-error: Argument of type 'NumberConstructor' is not
// assignable to parameter of type '(num: number) => string'.
// Type 'number' is not assignable to type 'string'.(2345)
stringify123(Number);Aber er akzeptiert diesen Funktionsaufruf
assert.equal(
stringify123(String), '123');TypeScript kann die Rückgabetypen von Funktionen normalerweise ableiten, aber die explizite Angabe ist erlaubt und gelegentlich nützlich (zumindest schadet sie nicht).
Für stringify123() ist die Angabe eines Rückgabetyps optional und sieht so aus
function stringify123(callback: (num: number) => string): string {
return callback(123);
}voidvoid ist ein spezieller Rückgabetyp für eine Funktion: Er sagt TypeScript, dass die Funktion immer undefined zurückgibt.
Sie kann dies explizit tun
function f1(): void {
return undefined;
}Oder sie kann dies implizit tun
function f2(): void {}Allerdings kann eine solche Funktion keine anderen Werte als undefined explizit zurückgeben.
function f3(): void {
// @ts-expect-error: Type '"abc"' is not assignable to type 'void'. (2322)
return 'abc';
}Ein Fragezeichen nach einem Bezeichner bedeutet, dass der Parameter optional ist. Zum Beispiel
function stringify123(callback?: (num: number) => string) {
if (callback === undefined) {
callback = String;
}
return callback(123); // (A)
}TypeScript erlaubt uns nur dann, den Funktionsaufruf in Zeile A durchzuführen, wenn wir sicherstellen, dass callback nicht undefined ist (was der Fall ist, wenn der Parameter weggelassen wurde).
TypeScript unterstützt Standardwerte für Parameter.
function createPoint(x=0, y=0): [number, number] {
return [x, y];
}
assert.deepEqual(
createPoint(),
[0, 0]);
assert.deepEqual(
createPoint(1, 2),
[1, 2]);Standardwerte machen Parameter optional. Wir können Typ-Annotationen normalerweise weglassen, da TypeScript die Typen ableiten kann. Zum Beispiel kann es ableiten, dass x und y beide den Typ number haben.
Wenn wir Typ-Annotationen hinzufügen wollten, würde das so aussehen.
function createPoint(x:number = 0, y:number = 0): [number, number] {
return [x, y];
}Wir können auch Rest-Parameter in TypeScript-Parameterdefinitionen verwenden. Ihre statischen Typen müssen Arrays sein (Listen oder Tupel).
function joinNumbers(...nums: number[]): string {
return nums.join('-');
}
assert.equal(
joinNumbers(1, 2, 3),
'1-2-3');Die Werte, die von einer Variablen gehalten werden (ein Wert nach dem anderen), können Mitglieder verschiedener Typen sein. In diesem Fall benötigen wir einen Union-Typ. Zum Beispiel ist in dem folgenden Code stringOrNumber entweder vom Typ string oder vom Typ number.
function getScore(stringOrNumber: string|number): number {
if (typeof stringOrNumber === 'string'
&& /^\*{1,5}$/.test(stringOrNumber)) {
return stringOrNumber.length;
} else if (typeof stringOrNumber === 'number'
&& stringOrNumber >= 1 && stringOrNumber <= 5) {
return stringOrNumber
} else {
throw new Error('Illegal value: ' + JSON.stringify(stringOrNumber));
}
}
assert.equal(getScore('*****'), 5);
assert.equal(getScore(3), 3);stringOrNumber hat den Typ string|number. Das Ergebnis des Typausdrucks s|t ist die mengentheoretische Vereinigung der Typen s und t (interpretiert als Mengen).
undefined und null nicht in Typen enthaltenIn vielen Programmiersprachen ist null Teil aller Objekttypen. Zum Beispiel kann in Java, wann immer der Typ einer Variablen String ist, dieser auf null gesetzt werden und Java wird sich nicht beschweren.
Umgekehrt werden in TypeScript undefined und null durch separate, disjunkte Typen behandelt. Wir benötigen Union-Typen wie undefined|string und null|string, wenn wir sie zulassen wollen.
let maybeNumber: null|number = null;
maybeNumber = 123;Andernfalls erhalten wir einen Fehler
// @ts-expect-error: Type 'null' is not assignable to type 'number'. (2322)
let maybeNumber: number = null;
maybeNumber = 123;Beachten Sie, dass TypeScript uns nicht zwingt, sofort zu initialisieren (solange wir nicht aus der Variablen lesen, bevor wir sie initialisieren).
let myNumber: number; // OK
myNumber = 123;Erinnern Sie sich an diese Funktion von vorhin
function stringify123(callback?: (num: number) => string) {
if (callback === undefined) {
callback = String;
}
return callback(123); // (A)
}Schreiben wir stringify123() neu, damit der Parameter callback nicht mehr optional ist: Wenn ein Aufrufer keine Funktion bereitstellen möchte, muss er explizit null übergeben. Das Ergebnis sieht wie folgt aus.
function stringify123(
callback: null | ((num: number) => string)) {
const num = 123;
if (callback === null) { // (A)
callback = String;
}
return callback(num); // (B)
}
assert.equal(
stringify123(null),
'123');
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
assert.throws(() => stringify123());Wiederum müssen wir den Fall behandeln, dass callback keine Funktion ist (Zeile A), bevor wir den Funktionsaufruf in Zeile B durchführen können. Hätten wir das nicht getan, hätte TypeScript in dieser Zeile einen Fehler gemeldet.
undefined|TDie folgenden drei Parametrierungen sind ziemlich ähnlich
x?: numberx = 456x: undefined | numberWenn der Parameter optional ist, kann er weggelassen werden. In diesem Fall hat er den Wert undefined.
function f1(x?: number) { return x }
assert.equal(f1(123), 123); // OK
assert.equal(f1(undefined), undefined); // OK
assert.equal(f1(), undefined); // can omitWenn der Parameter einen Standardwert hat, wird dieser Wert verwendet, wenn der Parameter entweder weggelassen oder auf undefined gesetzt wird.
function f2(x = 456) { return x }
assert.equal(f2(123), 123); // OK
assert.equal(f2(undefined), 456); // OK
assert.equal(f2(), 456); // can omitWenn der Parameter einen Union-Typ hat, kann er nicht weggelassen werden, aber wir können ihn auf undefined setzen.
function f3(x: undefined | number) { return x }
assert.equal(f3(123), 123); // OK
assert.equal(f3(undefined), undefined); // OK
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
f3(); // can’t omitÄhnlich wie Arrays spielen Objekte in JavaScript zwei Rollen (die gelegentlich gemischt werden)
Records: Eine feste Anzahl von Eigenschaften, die zur Entwicklungszeit bekannt sind. Jede Eigenschaft kann einen anderen Typ haben.
Dictionaries: Eine beliebige Anzahl von Eigenschaften, deren Namen zur Entwicklungszeit nicht bekannt sind. Alle Eigenschaften haben denselben Typ.
Objekte als Dictionaries ignorieren wir in diesem Kapitel – sie werden in §15.4.5 „Index-Signaturen: Objekte als Dictionaries“ behandelt. Übrigens sind Maps für Dictionaries sowieso meistens eine bessere Wahl.
Interfaces beschreiben Objekte als Records. Zum Beispiel
interface Point {
x: number;
y: number;
}Wir können Mitglieder auch durch Kommas trennen
interface Point {
x: number,
y: number,
}Ein großer Vorteil des Typsystems von TypeScript ist, dass es strukturell und nicht nominal arbeitet. Das heißt, das Interface Point passt zu allen Objekten, die die entsprechende Struktur aufweisen.
interface Point {
x: number;
y: number;
}
function pointToString(pt: Point) {
return `(${pt.x}, ${pt.y})`;
}
assert.equal(
pointToString({x: 5, y: 7}), // compatible structure
'(5, 7)');Umgekehrt muss in dem nominalen Typsystem von Java jede Klasse explizit deklarieren, welche Interfaces sie implementiert. Daher kann eine Klasse nur Interfaces implementieren, die zum Zeitpunkt ihrer Erstellung existieren.
Objekt-Literal-Typen sind anonyme Interfaces.
type Point = {
x: number;
y: number;
};Ein Vorteil von Objekt-Literal-Typen ist, dass sie inline verwendet werden können.
function pointToString(pt: {x: number, y: number}) {
return `(${pt.x}, ${pt.y})`;
}Wenn eine Eigenschaft weggelassen werden kann, setzen wir ein Fragezeichen hinter ihren Namen.
interface Person {
name: string;
company?: string;
}Im folgenden Beispiel passen sowohl john als auch jane zu dem Interface Person.
const john: Person = {
name: 'John',
};
const jane: Person = {
name: 'Jane',
company: 'Massive Dynamic',
};Interfaces können auch Methoden enthalten.
interface Point {
x: number;
y: number;
distance(other: Point): number;
}Was das Typsystem von TypeScript betrifft, sind Methoden-Definitionen und Eigenschaften, deren Werte Funktionen sind, gleichwertig.
interface HasMethodDef {
simpleMethod(flag: boolean): void;
}
interface HasFuncProp {
simpleMethod: (flag: boolean) => void;
}
const objWithMethod: HasMethodDef = {
simpleMethod(flag: boolean): void {},
};
const objWithMethod2: HasFuncProp = objWithMethod;
const objWithOrdinaryFunction: HasMethodDef = {
simpleMethod: function (flag: boolean): void {},
};
const objWithOrdinaryFunction2: HasFuncProp = objWithOrdinaryFunction;
const objWithArrowFunction: HasMethodDef = {
simpleMethod: (flag: boolean): void => {},
};
const objWithArrowFunction2: HasFuncProp = objWithArrowFunction;Meine Empfehlung ist, die Syntax zu verwenden, die am besten ausdrückt, wie eine Eigenschaft eingerichtet werden soll.
Erinnern wir uns an die beiden Sprachebenen von TypeScript.
Ähnlich gilt:
Normale Funktionen existieren auf der dynamischen Ebene, sind Fabriken für Werte und haben Parameter, die Werte repräsentieren. Parameter werden zwischen Klammern deklariert.
const valueFactory = (x: number) => x; // definition
const myValue = valueFactory(123); // useGenerische Typen existieren auf der statischen Ebene, sind Fabriken für Typen und haben Parameter, die Typen repräsentieren. Parameter werden zwischen Winkelklammern deklariert.
type TypeFactory<X> = X; // definition
type MyType = TypeFactory<string>; // use Typ-Parameter benennen
In TypeScript ist es üblich, ein einzelnes Großbuchstaben (wie T, I und O) für einen Typ-Parameter zu verwenden. Jeder legale JavaScript-Bezeichner ist jedoch zulässig und längere Namen machen den Code oft leichter verständlich.
// Factory for types
interface ValueContainer<Value> {
value: Value;
}
// Creating one type
type StringContainer = ValueContainer<string>;Value ist eine Typvariable. Eine oder mehrere Typvariablen können zwischen Winkelklammern eingeführt werden.
Auch Klassen können Typ-Parameter haben.
class SimpleStack<Elem> {
#data: Array<Elem> = [];
push(x: Elem): void {
this.#data.push(x);
}
pop(): Elem {
const result = this.#data.pop();
if (result === undefined) {
throw new Error();
}
return result;
}
get length() {
return this.#data.length;
}
}Die Klasse SimpleStack hat den Typ-Parameter Elem. Wenn wir die Klasse instanziieren, liefern wir auch einen Wert für den Typ-Parameter.
const stringStack = new SimpleStack<string>();
stringStack.push('first');
stringStack.push('second');
assert.equal(stringStack.length, 2);
assert.equal(stringStack.pop(), 'second');Maps sind in TypeScript generisch typisiert. Zum Beispiel
const myMap: Map<boolean,string> = new Map([
[false, 'no'],
[true, 'yes'],
]);Dank Typ-Inferenz (basierend auf dem Argument von new Map()) können wir die Typ-Parameter weglassen.
// %inferred-type: Map<boolean, string>
const myMap = new Map([
[false, 'no'],
[true, 'yes'],
]);Funktionsdefinitionen können Typvariablen wie folgt einführen.
function identity<Arg>(arg: Arg): Arg {
return arg;
}Wir verwenden die Funktion wie folgt.
// %inferred-type: number
const num1 = identity<number>(123);Aufgrund der Typ-Inferenz können wir den Typ-Parameter wieder weglassen.
// %inferred-type: 123
const num2 = identity(123);Beachten Sie, dass TypeScript den Typ 123 abgeleitet hat, der eine Menge mit einer Zahl ist und spezifischer als der Typ number.
Pfeilfunktionen können ebenfalls Typ-Parameter haben.
const identity = <Arg>(arg: Arg): Arg => arg;Dies ist die Syntax für Typ-Parameter für Methoden.
const obj = {
identity<Arg>(arg: Arg): Arg {
return arg;
},
};function fillArray<T>(len: number, elem: T): T[] {
return new Array<T>(len).fill(elem);
}Die Typvariable T erscheint viermal in diesem Code.
fillArray<T> eingeführt. Daher ist ihr Geltungsbereich die Funktion.elem verwendet.fillArray() anzugeben.Array() verwendet.Wir können den Typ-Parameter beim Aufruf von fillArray() (Zeile A) weglassen, da TypeScript T aus dem Parameter elem ableiten kann.
// %inferred-type: string[]
const arr1 = fillArray<string>(3, '*');
assert.deepEqual(
arr1, ['*', '*', '*']);
// %inferred-type: string[]
const arr2 = fillArray(3, '*'); // (A)Verwenden wir, was wir gelernt haben, um das zuvor gesehene Code-Fragment zu verstehen.
interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U
): U;
// ···
}Dies ist ein Interface für Arrays, deren Elemente vom Typ T sind.
Die Methode .concat() hat null oder mehr Parameter (definiert über einen Rest-Parameter). Jeder dieser Parameter hat den Typ T[]|T. Das heißt, es ist entweder ein Array von T-Werten oder ein einzelner T-Wert.
Die Methode .reduce() führt ihre eigene Typvariable U ein. U wird verwendet, um die Tatsache auszudrücken, dass die folgenden Entitäten alle denselben Typ haben.
state von callback()callback()firstState von .reduce().reduce()Zusätzlich zu state hat callback() die folgenden Parameter.
element, das denselben Typ T wie die Array-Elemente hat.index; eine Zahlarray mit Elementen vom Typ T.