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

23 Ein Überblick über Berechnungen mit Typen



In diesem Kapitel untersuchen wir, wie wir zur Kompilierzeit mit Typen in TypeScript rechnen können.

Beachten Sie, dass der Fokus dieses Kapitels auf dem Erlernen der Berechnung mit Typen liegt. Daher werden wir Literal-Typen häufig verwenden und die Beispiele sind weniger praktisch relevant.

23.1 Typen als Metawerte

Betrachten Sie die folgenden beiden Ebenen von TypeScript-Code

Die Typ-Ebene ist eine Meta-Ebene der Programmebene.

Ebene Verfügbar zur Operanden Operationen
Programmebene Laufzeit Werte Funktionen
Typ-Ebene Kompilierzeit Spezifische Typen Generische Typen

Was bedeutet es, dass wir mit Typen rechnen können? Der folgende Code ist ein Beispiel

type ObjectLiteralType = {
  first: 1,
  second: 2,
};

// %inferred-type: "first" | "second"
type Result = keyof ObjectLiteralType; // (A)

In Zeile A gehen wir die folgenden Schritte durch

Auf der Typ-Ebene können wir mit den folgenden „Werten“ rechnen

type ObjectLiteralType = {
  prop1: string,
  prop2: number,
};

interface InterfaceType {
  prop1: string;
  prop2: number;
}

type TupleType = [boolean, bigint];

//::::: Nullish types and literal types :::::
// Same syntax as values, but they are all types!

type UndefinedType = undefined;
type NullType = null;

type BooleanLiteralType = true;
type NumberLiteralType = 12.34;
type BigIntLiteralType = 1234n;
type StringLiteralType = 'abc';

23.2 Generische Typen: Fabriken für Typen

Generische Typen sind Funktionen auf der Meta-Ebene – zum Beispiel

type Wrap<T> = [T];

Der generische Typ Wrap<> hat den Parameter T. Sein Ergebnis ist T, verpackt in einem Tupel-Typ. So verwenden wir diese Metafunktion

// %inferred-type: [string]
type Wrapped = Wrap<string>;

Wir übergeben den Parameter string an Wrap<> und geben dem Ergebnis den Alias Wrapped. Das Ergebnis ist ein Tupel-Typ mit einer einzigen Komponente – dem Typ string.

23.3 Union-Typen und Intersection-Typen

23.3.1 Union-Typen (|)

Der Typoperator | wird verwendet, um Union-Typen zu erstellen

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// %inferred-type: "a" | "b" | "c" | "d"
type Union = A | B;

Wenn wir Typ A und Typ B als Mengen betrachten, dann ist A | B die mengentheoretische Vereinigung dieser Mengen. Anders ausgedrückt: Die Elemente des Ergebnisses sind Elemente von mindestens einem der Operanden.

Syntaktisch können wir auch ein | vor die erste Komponente eines Union-Typs setzen. Das ist praktisch, wenn sich eine Typdefinition über mehrere Zeilen erstreckt

type A =
  | 'a'
  | 'b'
  | 'c'
;
23.3.1.1 Vereinigungen als Sammlungen von Metawerten

TypeScript stellt Sammlungen von Metawerten als Vereinigungen von Literal-Typen dar. Davon haben wir bereits ein Beispiel gesehen

type Obj = {
  first: 1,
  second: 2,
};

// %inferred-type: "first" | "second"
type Result = keyof Obj;

Wir werden bald Typ-Level-Operationen zum Durchlaufen solcher Sammlungen sehen.

23.3.1.2 Vereinigungen von Objekttypen

Da jedes Mitglied eines Union-Typs Mitglied von *mindestens* einem der Komponententypen ist, können wir nur sicher auf Eigenschaften zugreifen, die von allen Komponententypen gemeinsam genutzt werden (Zeile A). Um auf andere Eigenschaften zuzugreifen, benötigen wir einen Typ-Guard (Zeile B)

type ObjectTypeA = {
  propA: bigint,
  sharedProp: string,
}
type ObjectTypeB = {
  propB: boolean,
  sharedProp: string,
}

type Union = ObjectTypeA | ObjectTypeB;

function func(arg: Union) {
  // string
  arg.sharedProp; // (A) OK
  // @ts-expect-error: Property 'propB' does not exist on type 'Union'.
  arg.propB; // error

  if ('propB' in arg) { // (B) type guard
    // ObjectTypeB
    arg;

    // boolean
    arg.propB;
  }
}

23.3.2 Intersection-Typen (&)

Der Typoperator & wird verwendet, um Intersection-Typen zu erstellen

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// %inferred-type: "b" | "c"
type Intersection = A & B;

Wenn wir Typ A und Typ B als Mengen betrachten, dann ist A & B der mengentheoretische Durchschnitt dieser Mengen. Anders ausgedrückt: Die Elemente des Ergebnisses sind Elemente beider Operanden.

23.3.2.1 Durchschnitte von Objekttypen

Der Durchschnitt zweier Objekttypen hat die Eigenschaften beider Typen

type Obj1 = { prop1: boolean };
type Obj2 = { prop2: number };
type Both = {
  prop1: boolean,
  prop2: number,
};

// Type Obj1 & Obj2 is assignable to type Both
// %inferred-type: true
type IntersectionHasBothProperties = IsAssignableTo<Obj1 & Obj2, Both>;

(Der generische Typ IsAssignableTo<> wird später erklärt.)

23.3.2.2 Verwendung von Intersection-Typen für Mixins

Wenn wir einen Objekttyp Named in einen anderen Typ Obj einmischen, benötigen wir einen Intersection-Typ (Zeile A)

interface Named {
  name: string;
}
function addName<Obj extends object>(obj: Obj, name: string)
  : Obj & Named // (A)
{
  const namedObj = obj as (Obj & Named);
  namedObj.name = name;
  return namedObj;
}

const obj = {
  last: 'Doe',
};

// %inferred-type: { last: string; } & Named
const namedObj = addName(obj, 'Jane');

23.4 Kontrollfluss

23.4.1 Bedingte Typen

Ein *bedingter Typ* hat die folgende Syntax

«Type2» extends «Type1» ? «ThenType» : «ElseType»

Wenn Type2 zu Type1 zuweisbar ist, dann ist das Ergebnis dieses Typausdrucks ThenType. Andernfalls ist es ElseType.

23.4.1.1 Beispiel: Nur Typen verpacken, die die Eigenschaft .length haben

Im folgenden Beispiel verpackt Wrap<> Typen nur in ein-elementige Tupel, wenn sie die Eigenschaft .length haben, deren Werte Zahlen sind

type Wrap<T> = T extends { length: number } ? [T] : T;

// %inferred-type: [string]
type A = Wrap<string>;

// %inferred-type: RegExp
type B = Wrap<RegExp>;
23.4.1.2 Beispiel: Überprüfung der Zuweisbarkeit

Wir können einen bedingten Typ verwenden, um eine Zuweisbarkeitsprüfung zu implementieren

type IsAssignableTo<A, B> = A extends B ? true : false;

// Type `123` is assignable to type `number`
// %inferred-type: true
type Result1 = IsAssignableTo<123, number>;

// Type `number` is not assignable to type `123`
// %inferred-type: false
type Result2 = IsAssignableTo<number, 123>;

Weitere Informationen zur Typbeziehung *Zuweisbarkeit* finden Sie unter [Inhalt nicht enthalten].

23.4.1.3 Bedingte Typen sind distributiv

Bedingte Typen sind *distributiv*: Das Anwenden eines bedingten Typs C auf einen Union-Typ U ist dasselbe wie die Vereinigung des Anwendens von C auf jede Komponente von U. Dies ist ein Beispiel

type Wrap<T> = T extends { length: number } ? [T] : T;

// %inferred-type: boolean | [string] | [number[]]
type C1 = Wrap<boolean | string | number[]>;

// Equivalent:
type C2 = Wrap<boolean> | Wrap<string> | Wrap<number[]>;

Mit anderen Worten, Distributivität ermöglicht es uns, über die Komponenten eines Union-Typs zu „loopen“.

Dies ist ein weiteres Beispiel für Distributivität

type AlwaysWrap<T> = T extends any ? [T] : [T];

// %inferred-type: ["a"] | ["d"] | [{ a: 1; } & { b: 2; }]
type Result = AlwaysWrap<'a' | ({ a: 1 } & { b: 2 }) | 'd'>;
23.4.1.4 Mit distributiven bedingten Typen verwenden wir den Typ never, um Dinge zu ignorieren

Als Menge interpretiert ist der Typ never leer. Daher wird er ignoriert, wenn er in einem Union-Typ vorkommt

// %inferred-type: "a" | "b"
type Result = 'a' | 'b' | never;

Das bedeutet, wir können never verwenden, um Komponenten eines Union-Typs zu ignorieren

type DropNumbers<T> = T extends number ? never : T;

// %inferred-type: "a" | "b"
type Result1 = DropNumbers<1 | 'a' | 2 | 'b'>;

Dies geschieht, wenn wir die Typausdrücke des dann- und des sonst-Zweigs vertauschen

type KeepNumbers<T> = T extends number ? T : never;

// %inferred-type: 1 | 2
type Result2 = KeepNumbers<1 | 'a' | 2 | 'b'>;
23.4.1.5 Eingebauter Hilfstyp: Exclude<T, U>

Das Ausschließen von Typen aus einer Vereinigung ist eine so häufige Operation, dass TypeScript den eingebauten Hilfstyp Exclude<T, U> bereitstellt

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

// %inferred-type: "a" | "b"
type Result1 = Exclude<1 | 'a' | 2 | 'b', number>;

// %inferred-type: "a" | 2
type Result2 = Exclude<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
23.4.1.6 Eingebauter Hilfstyp: Extract<T, U>

Das Gegenteil von Exclude<T, U> ist Extract<T, U>

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

// %inferred-type: 1 | 2
type Result1 = Extract<1 | 'a' | 2 | 'b', number>;

// %inferred-type: 1 | "b"
type Result2 = Extract<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
23.4.1.7 Verketten von bedingten Typen

Ähnlich wie der ternäre Operator von JavaScript können wir auch den bedingten Typoperator von TypeScript verketten

type LiteralTypeName<T> =
  T extends undefined ? "undefined" :
  T extends null ? "null" :
  T extends boolean ? "boolean" :
  T extends number ? "number" :
  T extends bigint ? "bigint" :
  T extends string ? "string" :
  never;

// %inferred-type: "bigint"
type Result1 = LiteralTypeName<123n>;

// %inferred-type: "string" | "number" | "boolean"
type Result2 = LiteralTypeName<true | 1 | 'a'>;
23.4.1.8 infer und bedingte Typen

https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-inference-in-conditional-types

Beispiel

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

Beispiel

type Syncify<Interf> = {
    [K in keyof Interf]:
        Interf[K] extends (...args: any[]) => Promise<infer Result>
        ? (...args: Parameters<Interf[K]>) => Result
        : Interf[K];
};

// Example:

interface AsyncInterface {
    compute(arg: number): Promise<boolean>;
    createString(): Promise<String>;
}

type SyncInterface = Syncify<AsyncInterface>;
    // type SyncInterface = {
    //     compute: (arg: number) => boolean;
    //     createString: () => String;
    // }

23.4.2 Gemappte Typen

Ein *gemappter Typ* erzeugt ein Objekt, indem er über eine Sammlung von Schlüsseln iteriert – zum Beispiel

// %inferred-type: { a: number; b: number; c: number; }
type Result = {
  [K in 'a' | 'b' | 'c']: number
};

Der Operator in ist ein entscheidender Teil eines gemappten Typs: Er gibt an, woher die Schlüssel für den neuen Objekt-Literal-Typ stammen.

23.4.2.1 Eingebauter Hilfstyp: Pick<T, K>

Der folgende eingebaute Hilfstyp ermöglicht es uns, ein neues Objekt zu erstellen, indem wir angeben, welche Eigenschaften eines vorhandenen Objekttyps wir beibehalten möchten

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

Er wird wie folgt verwendet

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

// %inferred-type: { eeny: 1; miny: 3; }
type Result = Pick<ObjectLiteralType, 'eeny' | 'miny'>;
23.4.2.2 Eingebauter Hilfstyp: Omit<T, K>

Der folgende eingebaute Hilfstyp ermöglicht es uns, einen neuen Objekttyp zu erstellen, indem wir angeben, welche Eigenschaften eines vorhandenen Objekttyps wir weglassen möchten

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Erläuterungen

Omit<> wird wie folgt verwendet

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

// %inferred-type: { meeny: 2; moe: 4; }
type Result = Omit<ObjectLiteralType, 'eeny' | 'miny'>;

23.5 Verschiedene andere Operatoren

23.5.1 Der Index-Typabfrageoperator keyof

Wir sind bereits auf den Typoperator keyof gestoßen. Er listet die Eigenschaftsschlüssel eines Objekttyps auf

type Obj = {
  0: 'a',
  1: 'b',
  prop0: 'c',
  prop1: 'd',
};

// %inferred-type: 0 | 1 | "prop0" | "prop1"
type Result = keyof Obj;

Die Anwendung von keyof auf einen Tupel-Typ hat ein möglicherweise etwas unerwartetes Ergebnis

// number | "0" | "1" | "2" | "length" | "pop" | "push" | ···
type Result = keyof ['a', 'b', 'c'];

Das Ergebnis umfasst

Die Eigenschaftsschlüssel eines leeren Objekt-Literal-Typs sind die leere Menge never

// %inferred-type: never
type Result = keyof {};

So behandelt keyof Intersection- und Union-Typen

type A = { a: number, shared: string };
type B = { b: number, shared: string };

// %inferred-type: "a" | "b" | "shared"
type Result1 = keyof (A & B);

// %inferred-type: "shared"
type Result2 = keyof (A | B);

Das ergibt Sinn, wenn wir uns daran erinnern, dass A & B die Eigenschaften von *beiden* Typen A und B hat. A und B haben nur die Eigenschaft .shared gemeinsam, was Result2 erklärt.

23.5.2 Der indizierte Zugriffsoperator T[K]

Der indizierte Zugriffsoperator T[K] gibt die Typen aller Eigenschaften von T zurück, deren Schlüssel zu Typ K zuweisbar sind. T[K] wird auch als *Lookup-Typ* bezeichnet.

Dies sind Beispiele für die Verwendung des Operators

type Obj = {
  0: 'a',
  1: 'b',
  prop0: 'c',
  prop1: 'd',
};

// %inferred-type: "a" | "b"
type Result1 = Obj[0 | 1];

// %inferred-type: "c" | "d"
type Result2 = Obj['prop0' | 'prop1'];

// %inferred-type: "a" | "b" | "c" | "d"
type Result3 = Obj[keyof Obj];

Der Typ in Klammern muss zu dem Typ aller Eigenschaftsschlüssel (wie von keyof berechnet) zuweisbar sein. Deshalb sind Obj[number] und Obj[string] nicht erlaubt. Wir können jedoch number und string als Index-Typen verwenden, wenn der indizierte Typ eine Index-Signatur hat (Zeile A)

type Obj = {
  [key: string]: RegExp, // (A)
};

// %inferred-type: string | number
type KeysOfObj = keyof Obj;

// %inferred-type: RegExp
type ValuesOfObj = Obj[string];

KeysOfObj enthält den Typ number, da Zahlenschlüssel in JavaScript (und damit in TypeScript) eine Untermenge von String-Schlüsseln sind.

Tupel-Typen unterstützen ebenfalls indizierten Zugriff

type Tuple = ['a', 'b', 'c', 'd'];

// %inferred-type:  "a" | "b"
type Elements = Tuple[0 | 1];

Der Klammeroperator ist ebenfalls distributiv

type MyType = { prop: 1 } | { prop: 2 } | { prop: 3 };

// %inferred-type: 1 | 2 | 3
type Result1 = MyType['prop'];

// Equivalent:
type Result2 =
  | { prop: 1 }['prop']
  | { prop: 2 }['prop']
  | { prop: 3 }['prop']
;

23.5.3 Der Typabfrageoperator typeof

Der Typoperator typeof wandelt einen (JavaScript-)Wert in seinen (TypeScript-)Typ um. Sein Operand muss ein Bezeichner oder eine Folge von punktgetrennten Bezeichnern sein

const str = 'abc';

// %inferred-type: "abc"
type Result = typeof str;

Das erste 'abc' ist ein Wert, während das zweite "abc" sein Typ ist, ein String-Literal-Typ.

Dies ist ein weiteres Beispiel für die Verwendung von typeof

const func = (x: number) => x + x;
// %inferred-type: (x: number) => number
type Result = typeof func;

§14.1.2 „Hinzufügen eines Symbols zu einem Typ“ beschreibt einen interessanten Anwendungsfall für typeof.