|)&)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.
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
ObjectLiteralType, ein Objekt-Literal-Typ.keyof auf die Eingabe an. Sie listet die Eigenschaftsschlüssel eines Objekttyps auf.keyof den Namen Result.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';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.
|)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'
;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.
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;
}
}&)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.
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.)
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');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.
.length habenIm 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>;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].
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'>;never, um Dinge zu ignorierenAls 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'>;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'>;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'>;Ä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'>;infer und bedingte Typenhttps://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;
// }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.
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'>;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
K extends keyof any bedeutet, dass K eine Unterart des Typs aller Eigenschaftsschlüssel sein muss
// %inferred-type: string | number | symbol
type Result = keyof any;Exclude<keyof T, K>> bedeutet: Nimm die Schlüssel von T und entferne alle „Werte“, die in K genannt werden.
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'>;keyofWir 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
"0" | "1" | "2"number der Index-Eigenschaftsschlüssel.lengthArray-Methoden: "pop" | "push" | ···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.
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']
;typeofDer 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.