TypeScript meistern
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

7 Die Grundlagen von TypeScript



Dieses Kapitel erklärt die Grundlagen von TypeScript.

7.1 Was Sie lernen werden

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.

7.2 Festlegen der Ausführlichkeit der Typprüfung

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

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.

7.3 Typen in TypeScript

In diesem Kapitel ist ein Typ einfach eine Menge von Werten. Die JavaScript-Sprache (nicht TypeScript!) hat nur acht Typen

  1. Undefined: die Menge mit dem einzigen Element undefined
  2. Null: die Menge mit dem einzigen Element null
  3. Boolean: die Menge mit den beiden Elementen false und true
  4. Number: die Menge aller Zahlen
  5. BigInt: die Menge aller Ganzzahlen mit beliebiger Genauigkeit
  6. String: die Menge aller Zeichenketten
  7. Symbol: die Menge aller Symbole
  8. Object: die Menge aller Objekte (was Funktionen und Arrays einschließt)

All 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.

7.4 Typ-Annotationen

function toString(num: number): string {
  return String(num);
}

In der vorherigen Funktionsdeklaration gibt es zwei Typ-Annotationen

Sowohl number als auch string sind Typausdrücke, die die Typen von Speicherstellen angeben.

7.5 Typ-Inferenz

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);
}

7.6 Festlegen von Typen über Typausdrücke

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

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.

7.7 Die beiden Sprachebenen: dynamisch vs. statisch

TypeScript hat zwei Sprachebenen

Diese beiden Ebenen sehen wir in der Syntax

const undef: undefined = undefined;

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.

7.8 Typ-Aliase

Mit type können wir einen neuen Namen (einen Alias) für einen vorhandenen Typ erstellen

type Age = number;
const age: Age = 82;

7.9 Arrays typisieren

Arrays spielen zwei Rollen in JavaScript (entweder eine oder beide)

7.9.1 Arrays als Listen

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.

7.9.2 Arrays als Tupel

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.

7.10 Funktionstypen

Dies ist ein Beispiel für einen Funktionstyp

(num: number) => string

Dieser 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.

7.10.1 Ein komplizierteres Beispiel

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');

7.10.2 Rückgabetypen von Funktionsdeklarationen

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);
}
7.10.2.1 Der spezielle Rückgabetyp void

void 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';
}

7.10.3 Optionale Parameter

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).

7.10.3.1 Standardwerte für Parameter

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];
}

7.10.4 Rest-Parameter

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');

7.11 Union-Typen

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).

7.11.1 Standardmäßig sind undefined und null nicht in Typen enthalten

In 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;

7.11.2 Auslassungen explizit machen

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.

7.12 Optional vs. Standardwert vs. undefined|T

Die folgenden drei Parametrierungen sind ziemlich ähnlich

Wenn 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 omit

Wenn 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 omit

Wenn 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

7.13 Objekte typisieren

Ähnlich wie Arrays spielen Objekte in JavaScript zwei Rollen (die gelegentlich gemischt werden)

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.

7.13.1 Objekte als Records über Interfaces typisieren

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,
}

7.13.2 Strukturelle Typisierung von TypeScript vs. nominale Typisierung

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.

7.13.3 Objekt-Literal-Typen

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})`;
}

7.13.4 Optionale Eigenschaften

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',
};

7.13.5 Methoden

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.

7.14 Typvariablen und generische Typen

Erinnern wir uns an die beiden Sprachebenen von TypeScript.

Ähnlich gilt:

  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.

7.14.1 Beispiel: ein Container für Werte

// 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.

7.15 Beispiel: eine generische Klasse

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');

7.15.1 Beispiel: Maps

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'],
]);

7.15.2 Typvariablen für Funktionen und Methoden

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.

7.15.2.1 Pfeilfunktionen und Methoden

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;
  },
};

7.15.3 Ein komplizierteres Funktionsbeispiel

function fillArray<T>(len: number, elem: T): T[] {
  return new Array<T>(len).fill(elem);
}

Die Typvariable T erscheint viermal in diesem Code.

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)

7.16 Fazit: das anfängliche Beispiel verstehen

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.