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

14 Hinzufügen spezieller Werte zu Typen



Man kann Typen als Mengen von Werten verstehen. Manchmal gibt es zwei Ebenen von Werten

In diesem Kapitel untersuchen wir, wie wir speziellen Werten auf Basisebene Typen hinzufügen können.

14.1 Hinzufügen spezieller Werte „in band“

Eine Möglichkeit, spezielle Werte hinzuzufügen, besteht darin, einen neuen Typ zu erstellen, der eine Obermenge des Basistyps ist, wobei einige Werte speziell sind. Diese speziellen Werte werden Sentinels genannt. Sie existieren in band (stellen Sie sich vor, im selben Kanal), als Geschwister normaler Werte.

Betrachten Sie als Beispiel die folgende Schnittstelle für lesbare Streams

interface InputStream {
  getNextLine(): string;
}

Im Moment behandelt .getNextLine() nur Textzeilen, aber keine Dateiendpunkte (EOFs). Wie könnten wir EOF-Unterstützung hinzufügen?

Möglichkeiten sind

Die nächsten beiden Unterabschnitte beschreiben zwei Möglichkeiten, wie wir Sentinel-Werte einführen können.

14.1.1 Hinzufügen von null oder undefined zu einem Typ

Bei der Verwendung von strengem TypeScript enthält kein einfacher Objekttyp (definiert über Schnittstellen, Objektmuster, Klassen usw.) null. Das macht es zu einem guten Sentinel-Wert, den wir dem Basistyp string über einen Vereinigungs-Typ hinzufügen können

type StreamValue = null | string;

interface InputStream {
  getNextLine(): StreamValue;
}

Immer wenn wir den von .getNextLine() zurückgegebenen Wert verwenden, zwingt uns TypeScript, beide Möglichkeiten in Betracht zu ziehen: Strings und null – zum Beispiel

function countComments(is: InputStream) {
  let commentCount = 0;
  while (true) {
    const line = is.getNextLine();
    // @ts-expect-error: Object is possibly 'null'.(2531)
    if (line.startsWith('#')) { // (A)
      commentCount++;
    }
    if (line === null) break;
  }
  return commentCount;
}

In Zeile A können wir die String-Methode .startsWith() nicht verwenden, da line möglicherweise null ist. Dies können wir wie folgt beheben

function countComments(is: InputStream) {
  let commentCount = 0;
  while (true) {
    const line = is.getNextLine();
    if (line === null) break;
    if (line.startsWith('#')) { // (A)
      commentCount++;
    }
  }
  return commentCount;
}

Wenn die Ausführung nun Zeile A erreicht, können wir sicher sein, dass line nicht null ist.

14.1.2 Hinzufügen eines Symbols zu einem Typ

Wir können auch andere Werte als null als Sentinels verwenden. Symbole und Objekte eignen sich am besten für diese Aufgabe, da jeder von ihnen eine eindeutige Identität hat und kein anderer Wert mit ihm verwechselt werden kann.

So verwenden Sie ein Symbol, um EOF darzustellen

const EOF = Symbol('EOF');
type StreamValue = typeof EOF | string;

Warum brauchen wir typeof und können nicht direkt EOF verwenden? Das liegt daran, dass EOF ein Wert und kein Typ ist. Der Typoperator typeof konvertiert EOF in einen Typ. Weitere Informationen zu den verschiedenen Sprachebenen von Werten und Typen finden Sie in §7.7 „Die beiden Sprachebenen: dynamisch vs. statisch“.

14.2 Hinzufügen spezieller Werte „out of band“

Was tun wir, wenn potenziell *jeder* Wert von einer Methode zurückgegeben werden kann? Wie stellen wir sicher, dass Basiswerte und Meta-Werte nicht vermischt werden? Hier ist ein Beispiel, bei dem das passieren könnte

interface InputStream<T> {
  getNextValue(): T;
}

Welchen Wert wir auch immer für EOF wählen, es besteht die Gefahr, dass jemand einen InputStream<typeof EOF> erstellt und diesen Wert dem Stream hinzufügt.

Die Lösung besteht darin, normale Werte und spezielle Werte getrennt zu halten, damit sie nicht vermischt werden. Spezielle Werte, die separat existieren, werden als out of band bezeichnet (stellen Sie sich einen anderen Kanal vor).

14.2.1 Diskriminierte Vereinigungen (Discriminated Unions)

Eine diskriminierte Vereinigung ist ein Vereinigungs-Typ über mehrere Objekttypen, die alle mindestens eine Eigenschaft gemeinsam haben, die sogenannte *Diskriminante*. Die Diskriminante muss für jeden Objekttyp einen anderen Wert haben – wir können sie als die ID des Objekttyps betrachten.

14.2.1.1 Beispiel: InputStreamValue

Im folgenden Beispiel ist InputStreamValue<T> eine diskriminierte Vereinigung und ihre Diskriminante ist .type.

interface NormalValue<T> {
  type: 'normal'; // string literal type
  data: T;
}
interface Eof {
  type: 'eof'; // string literal type
}
type InputStreamValue<T> = Eof | NormalValue<T>;

interface InputStream<T> {
  getNextValue(): InputStreamValue<T>;
}
function countValues<T>(is: InputStream<T>, data: T) {
  let valueCount = 0;
  while (true) {
    // %inferred-type: Eof | NormalValue<T>
    const value = is.getNextValue(); // (A)

    if (value.type === 'eof') break;

    // %inferred-type: NormalValue<T>
    value; // (B)

    if (value.data === data) { // (C)
      valueCount++;
    }
  }
  return valueCount;
}

Anfänglich ist der Typ von value InputStreamValue<T> (Zeile A). Dann schließen wir den Wert 'eof' für die Diskriminante .type aus, und sein Typ wird auf NormalValue<T> verengt (Zeile B). Deshalb können wir in Zeile C auf die Eigenschaft .data zugreifen.

14.2.1.2 Beispiel: IteratorResult

Bei der Entscheidung, wie Iteratoren implementiert werden sollen, wollte TC39 keinen festen Sentinel-Wert verwenden. Andernfalls könnte dieser Wert in Iterables erscheinen und Code brechen. Eine Lösung wäre gewesen, einen Sentinel-Wert bei Beginn einer Iteration zu wählen. TC39 entschied sich stattdessen für eine diskriminierte Vereinigung mit der gemeinsamen Eigenschaft .done

interface IteratorYieldResult<TYield> {
  done?: false; // boolean literal type
  value: TYield;
}

interface IteratorReturnResult<TReturn> {
  done: true; // boolean literal type
  value: TReturn;
}

type IteratorResult<T, TReturn = any> =
  | IteratorYieldResult<T>
  | IteratorReturnResult<TReturn>;

14.2.2 Andere Arten von Vereinigungs-Typen (Union Types)

Andere Arten von Vereinigungs-Typen können so praktisch sein wie diskriminierte Vereinigungen, solange wir die Mittel haben, die Mitgliedstypen der Vereinigung zu unterscheiden.

Eine Möglichkeit besteht darin, die Mitgliedstypen über eindeutige Eigenschaften zu unterscheiden

interface A {
  one: number;
  two: number;
}
interface B {
  three: number;
  four: number;
}
type Union = A | B;

function func(x: Union) {
  // @ts-expect-error: Property 'two' does not exist on type 'Union'.
  // Property 'two' does not exist on type 'B'.(2339)
  console.log(x.two); // error
  
  if ('one' in x) { // discriminating check
    console.log(x.two); // OK
  }
}

Eine weitere Möglichkeit besteht darin, die Mitgliedstypen über typeof und/oder Instanzprüfungen zu unterscheiden

type Union = [string] | number;

function logHexValue(x: Union) {
  if (Array.isArray(x)) { // discriminating check
    console.log(x[0]); // OK
  } else {
    console.log(x.toString(16)); // OK
  }
}