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

19 Arrays typisieren



In diesem Kapitel untersuchen wir, wie Arrays in TypeScript typisiert werden können.

19.1 Rollen von Arrays

Arrays können in JavaScript die folgenden Rollen spielen (entweder eine oder eine Mischung davon)

TypeScript unterstützt diese beiden Rollen, indem es verschiedene Möglichkeiten zur Typisierung von Arrays bietet. Diese werden wir als Nächstes betrachten.

19.2 Arten der Array-Typisierung

19.2.1 Array-Rolle „Liste“: Array-Typ-Literale vs. Interface-Typ Array

Ein Array-Typ-Literal besteht aus dem Elementtyp gefolgt von []. Im folgenden Code ist das Array-Typ-Literal string[]

// Each Array element has the type `string`:
const myStringArray: string[] = ['fee', 'fi', 'fo', 'fum'];

Ein Array-Typ-Literal ist eine Kurzform für die Verwendung des globalen generischen Interface-Typs Array

const myStringArray: Array<string> = ['fee', 'fi', 'fo', 'fum'];

Wenn der Elementtyp komplizierter ist, benötigen wir Klammern für Array-Typ-Literale

(number|string)[]
(() => boolean)[]

Der generische Typ Array funktioniert in diesem Fall besser

Array<number|string>
Array<() => boolean>

19.2.2 Array-Rolle „Tupel“: Tupel-Typ-Literale

Wenn das Array eine feste Länge hat und jedes Element einen anderen, festen Typ hat, der von seiner Position abhängt, können wir Tupel-Typ-Literale wie [string, string, boolean] verwenden.

const yes: [string, string, boolean] = ['oui', 'sí', true];

19.2.3 Objekte, die auch Array-ähnlich sind: Interfaces mit Index-Signaturen

Wenn ein Interface nur eine Index-Signatur hat, können wir es für Arrays verwenden.

interface StringArray {
  [index: number]: string;
}
const strArr: StringArray = ['Huey', 'Dewey', 'Louie'];

Ein Interface, das sowohl eine Index-Signatur als auch Eigenschafts-Signaturen hat, funktioniert nur für Objekte (da indizierte Elemente und Eigenschaften gleichzeitig definiert werden müssen).

interface FirstNamesAndLastName {
  [index: number]: string;
  lastName: string;
}

const ducks: FirstNamesAndLastName = {
  0: 'Huey',
  1: 'Dewey',
  2: 'Louie',
  lastName: 'Duck',
};

19.3 Fallstrick: Typinferenz ist bei Array-Typen nicht immer richtig

19.3.1 Typen von Arrays zu inferieren ist schwierig

Aufgrund der beiden Rollen von Arrays ist es für TypeScript unmöglich, immer den richtigen Typ zu erraten. Betrachten wir als Beispiel das folgende Array-Literal, das der Variablen fields zugewiesen wird.

const fields: Fields = [
  ['first', 'string', true],
  ['last', 'string', true],
  ['age', 'number', false],
];

Was ist der beste Typ für fields? Die folgenden sind alle vernünftige Optionen.

type Fields = Array<[string, string, boolean]>;
type Fields = Array<[string, ('string'|'number'), boolean]>;
type Fields = Array<Array<string|boolean>>;
type Fields = [
  [string, string, boolean],
  [string, string, boolean],
  [string, string, boolean],
];
type Fields = [
  [string, 'string', boolean],
  [string, 'string', boolean],
  [string, 'number', boolean],
];
type Fields = [
  Array<string|boolean>,
  Array<string|boolean>,
  Array<string|boolean>,
];

19.3.2 Typinferenz für nicht-leere Array-Literale

Wenn wir nicht-leere Array-Literale verwenden, ist die Standardeinstellung von TypeScript die Inferenz von Listentypen (nicht Tupeltypen).

// %inferred-type: (string | number)[]
const arr = [123, 'abc'];

Leider ist das nicht immer das, was wir wollen.

function func(p: [number, number]) {
  return p;
}
// %inferred-type: number[]
const pair1 = [1, 2];

// @ts-expect-error: Argument of type 'number[]' is not assignable to
// parameter of type '[number, number]'. [...]
func(pair1);

Wir können dies beheben, indem wir der const-Deklaration eine Typannotation hinzufügen, die die Typinferenz vermeidet.

const pair2: [number, number] = [1, 2];
func(pair2); // OK

19.3.3 Typinferenz für leere Array-Literale

Wenn wir eine Variable mit einem leeren Array-Literal initialisieren, inferiert TypeScript zunächst den Typ any[] und aktualisiert diesen Typ inkrementell, wenn wir Änderungen vornehmen.

// %inferred-type: any[]
const arr1 = [];

arr1.push(123);
// %inferred-type: number[]
arr1;

arr1.push('abc');
// %inferred-type: (string | number)[]
arr1;

Beachten Sie, dass der anfängliche inferierte Typ nicht von dem beeinflusst wird, was später passiert.

Wenn wir statt .push() eine Zuweisung verwenden, funktioniert es genauso.

// %inferred-type: any[]
const arr1 = [];

arr1[0] = 123;
// %inferred-type: number[]
arr1;

arr1[1] = 'abc';
// %inferred-type: (string | number)[]
arr1;

Wenn das Array-Literal hingegen mindestens ein Element hat, ist der Elementtyp fixiert und ändert sich später nicht.

// %inferred-type: number[]
const arr = [123];

// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'number'. (2345)
arr.push('abc');

19.3.4 const-Assertionen für Arrays und Typinferenz

Wir können ein Array-Literal mit einer const-Assertion suffixen.

// %inferred-type: readonly ["igneous", "metamorphic", "sedimentary"]
const rockCategories =
  ['igneous', 'metamorphic', 'sedimentary'] as const;

Wir erklären, dass rockCategories sich nicht ändern wird. Das hat folgende Auswirkungen:

Hier sind weitere Beispiele für Array-Literale mit und ohne const-Assertionen.

// %inferred-type: readonly [1, 2, 3, 4]
const numbers1 = [1, 2, 3, 4] as const;
// %inferred-type: number[]
const numbers2 = [1, 2, 3, 4];

// %inferred-type: readonly [true, "abc"]
const booleanAndString1 = [true, 'abc'] as const;
// %inferred-type: (string | boolean)[]
const booleanAndString2 = [true, 'abc'];
19.3.4.1 Mögliche Fallstricke von const-Assertionen

Es gibt zwei mögliche Fallstricke bei const-Assertionen.

Erstens ist der inferierte Typ so eng wie möglich. Das verursacht ein Problem für Variablen, die mit let deklariert wurden: Wir können kein anderes Tupel zuweisen als das, das wir zur Initialisierung verwendet haben.

let arr = [1, 2] as const;

arr = [1, 2]; // OK

// @ts-expect-error: Type '3' is not assignable to type '2'. (2322)
arr = [1, 3];

Zweitens können Tupel, die über as const deklariert wurden, nicht mutiert werden.

let arr = [1, 2] as const;

// @ts-expect-error: Cannot assign to '1' because it is a read-only
// property. (2540)
arr[1] = 3;

Das ist weder ein Vorteil noch ein Nachteil, aber wir müssen uns bewusst sein, dass es passiert.

19.4 Fallstrick: TypeScript geht davon aus, dass Indizes nie außerhalb der Grenzen liegen

Immer wenn wir über einen Index auf ein Array-Element zugreifen, geht TypeScript immer davon aus, dass der Index im Bereich liegt (Zeile A).

const messages: string[] = ['Hello'];

// %inferred-type: string
const message = messages[3]; // (A)

Aufgrund dieser Annahme ist der Typ von message string. Und nicht undefined oder undefined|string, wie wir vielleicht erwartet hätten.

Wir erhalten einen Fehler, wenn wir einen Tupeltyp verwenden.

const messages: [string] = ['Hello'];

// @ts-expect-error: Tuple type '[string]' of length '1' has no element
// at index '1'. (2493)
const message = messages[1];

as const hätte denselben Effekt gehabt, da es zur Inferenz eines Tupeltyps führt.