===)typeof, instanceof, Array.isArrayin@hqoss/guards: Bibliothek mit Typ-SchutzfunktionenIn TypeScript kann ein Wert einen Typ haben, der für bestimmte Operationen zu allgemein ist – zum Beispiel ein Union-Typ. Dieses Kapitel beantwortet folgende Fragen
T eines Speicherorts (wie einer Variablen oder einer Eigenschaft) auf eine Untermenge von T zu ändern. So ist es oft nützlich, den Typ null|string auf den Typ string zu verengen.typeof und instanceof sind Typ-Schutzfunktionen.Um zu sehen, wie ein statischer Typ zu allgemein sein kann, betrachten wir die folgende Funktion getScore()
assert.equal(
getScore('*****'), 5);
assert.equal(
getScore(3), 3);Das Grundgerüst von getScore() sieht wie folgt aus
function getScore(value: number|string): number {
// ···
}Im Inneren von getScore() wissen wir nicht, ob der Typ von value number oder string ist. Bevor wir das wissen, können wir mit value nicht wirklich arbeiten.
if und Typ-SchutzfunktionenDie Lösung besteht darin, den Typ von value zur Laufzeit über typeof zu überprüfen (Zeile A und Zeile B)
function getScore(value: number|string): number {
if (typeof value === 'number') { // (A)
// %inferred-type: number
value;
return value;
}
if (typeof value === 'string') { // (B)
// %inferred-type: string
value;
return value.length;
}
throw new Error('Unsupported value: ' + value);
}In diesem Kapitel interpretieren wir Typen als Mengen von Werten. (Weitere Informationen zu dieser und einer anderen Interpretation finden Sie in [Inhalt nicht enthalten].)
Innerhalb der then-Blöcke ab Zeile A und Zeile B ändert sich der statische Typ von value aufgrund der durchgeführten Prüfungen. Wir arbeiten nun mit Teilmengen des ursprünglichen Typs number|string. Diese Art der Verkleinerung eines Typs nennt man *Verengung*. Das Prüfen des Ergebnisses von typeof und ähnlichen Laufzeitoperationen nennt man *Typ-Schutzfunktionen*.
Beachten Sie, dass die Verengung den ursprünglichen Typ von value nicht ändert, sondern ihn nur spezifischer macht, je mehr Prüfungen wir durchlaufen.
switch und eine Typ-SchutzfunktionDie Verengung funktioniert auch, wenn wir switch anstelle von if verwenden
function getScore(value: number|string): number {
switch (typeof value) {
case 'number':
// %inferred-type: number
value;
return value;
case 'string':
// %inferred-type: string
value;
return value.length;
default:
throw new Error('Unsupported value: ' + value);
}
}Dies sind weitere Beispiele für zu allgemeine Typen
Nullable Typen
function func1(arg: null|string) {}
function func2(arg: undefined|string) {}Diskriminierte Unions
type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;
function func3(attendee: Attendee) {}Typen von optionalen Parametern
function func4(arg?: string) {
// %inferred-type: string | undefined
arg;
}Beachten Sie, dass diese Typen alle Union-Typen sind!
unknownWenn ein Wert den Typ unknown hat, können wir fast nichts damit tun und müssen seinen Typ zuerst verengen (Zeile A)
function parseStringLiteral(stringLiteral: string): string {
const result: unknown = JSON.parse(stringLiteral);
if (typeof result === 'string') { // (A)
return result;
}
throw new Error('Not a string literal: ' + stringLiteral);
}Mit anderen Worten: Der Typ unknown ist zu allgemein und wir müssen ihn verengen. In gewisser Weise ist unknown auch ein Union-Typ (die Union aller Typen).
Wie wir gesehen haben, ist eine Typ-Schutzfunktion eine Operation, die entweder true oder false zurückgibt – je nachdem, ob ihr Operand zur Laufzeit bestimmte Kriterien erfüllt. Die Typinferenz von TypeScript unterstützt Typ-Schutzfunktionen, indem sie den statischen Typ eines Operanden verengt, wenn das Ergebnis true ist.
===)Strikte Gleichheit funktioniert als Typ-Schutzfunktion
function func(value: unknown) {
if (value === 'abc') {
// %inferred-type: "abc"
value;
}
}Bei einigen Union-Typen können wir === verwenden, um zwischen ihren Komponenten zu unterscheiden
interface Book {
title: null | string;
isbn: string;
}
function getTitle(book: Book) {
if (book.title === null) {
// %inferred-type: null
book.title;
return '(Untitled)';
} else {
// %inferred-type: string
book.title;
return book.title;
}
}Die Verwendung von === zum Einbeziehen und !=== zum Ausschließen einer Komponente eines Union-Typs funktioniert nur, wenn diese Komponente ein Singleton-Typ ist (eine Menge mit einem Mitglied). Der Typ null ist ein Singleton-Typ. Sein einziges Mitglied ist der Wert null.
typeof, instanceof, Array.isArrayDies sind drei gängige eingebaute Typ-Schutzfunktionen
function func(value: Function|Date|number[]) {
if (typeof value === 'function') {
// %inferred-type: Function
value;
}
if (value instanceof Date) {
// %inferred-type: Date
value;
}
if (Array.isArray(value)) {
// %inferred-type: number[]
value;
}
}Beachten Sie, wie der statische Typ von value innerhalb der then-Blöcke verengt wird.
inWenn der Operator in zur Prüfung auf unterschiedliche Eigenschaften verwendet wird, ist er eine Typ-Schutzfunktion
type FirstOrSecond =
| {first: string}
| {second: string};
function func(firstOrSecond: FirstOrSecond) {
if ('second' in firstOrSecond) {
// %inferred-type: { second: string; }
firstOrSecond;
}
}Beachten Sie, dass die folgende Prüfung nicht funktioniert hätte
function func(firstOrSecond: FirstOrSecond) {
// @ts-expect-error: Property 'second' does not exist on
// type 'FirstOrSecond'. [...]
if (firstOrSecond.second !== undefined) {
// ···
}
}Das Problem in diesem Fall ist, dass wir ohne Verengung nicht auf die Eigenschaft .second eines Werts zugreifen können, dessen Typ FirstOrSecond ist.
in verengt keine nicht-Union-TypenLeider hilft uns in nur bei Union-Typen
function func(obj: object) {
if ('name' in obj) {
// %inferred-type: object
obj;
// @ts-expect-error: Property 'name' does not exist on type 'object'.
obj.name;
}
}Bei einer diskriminierten Union haben die Komponenten eines Union-Typs eine oder mehrere gemeinsame Eigenschaften, deren Werte für jede Komponente unterschiedlich sind. Solche Eigenschaften werden als Diskriminanten bezeichnet.
Die Prüfung des Werts eines Diskriminanten ist eine Typ-Schutzfunktion
type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;
function getId(attendee: Attendee) {
switch (attendee.kind) {
case 'Teacher':
// %inferred-type: { kind: "Teacher"; teacherId: string; }
attendee;
return attendee.teacherId;
case 'Student':
// %inferred-type: { kind: "Student"; studentId: string; }
attendee;
return attendee.studentId;
default:
throw new Error();
}
}Im vorherigen Beispiel ist .kind ein Diskriminant: Jede Komponente des Union-Typs Attendee hat diese Eigenschaft mit einem eindeutigen Wert.
Eine if-Anweisung und Gleichheitsprüfungen funktionieren ähnlich wie eine switch-Anweisung
function getId(attendee: Attendee) {
if (attendee.kind === 'Teacher') {
// %inferred-type: { kind: "Teacher"; teacherId: string; }
attendee;
return attendee.teacherId;
} else if (attendee.kind === 'Student') {
// %inferred-type: { kind: "Student"; studentId: string; }
attendee;
return attendee.studentId;
} else {
throw new Error();
}
}Wir können auch die Typen von Eigenschaften verengen (sogar von verschachtelten, auf die wir über eine Kette von Eigenschaftsnamen zugreifen)
type MyType = {
prop?: number | string,
};
function func(arg: MyType) {
if (typeof arg.prop === 'string') {
// %inferred-type: string
arg.prop; // (A)
[].forEach((x) => {
// %inferred-type: string | number | undefined
arg.prop; // (B)
});
// %inferred-type: string
arg.prop;
arg = {};
// %inferred-type: string | number | undefined
arg.prop; // (C)
}
}Werfen wir einen Blick auf mehrere Stellen im vorherigen Code
arg.prop über eine Typ-Schutzfunktion verengt..every() verengt nichtWenn wir .every() verwenden, um zu prüfen, ob alle Array-Elemente nicht-nullish sind, verengt TypeScript den Typ von mixedValues nicht (Zeile A)
const mixedValues: ReadonlyArray<undefined|null|number> =
[1, undefined, 2, null];
if (mixedValues.every(isNotNullish)) {
// %inferred-type: readonly (number | null | undefined)[]
mixedValues; // (A)
}Beachten Sie, dass mixedValues schreibgeschützt sein muss. Wäre dies nicht der Fall, würde eine andere Referenz darauf uns statisch erlauben, null in mixedValues innerhalb der if-Anweisung einzufügen. Aber das würde den verengten Typ von mixedValues falsch machen.
Der vorherige Code verwendet die folgende *benutzerdefinierte Typ-Schutzfunktion* (mehr dazu bald)
function isNotNullish<T>(value: T): value is NonNullable<T> { // (A)
return value !== undefined && value !== null;
}NonNullable<Union> (Zeile A) ist ein Utility-Typ, der die Typen undefined und null aus dem Union-Typ Union entfernt.
.filter() erzeugt Arrays mit verengten Typen.filter() erzeugt Arrays mit verengten Typen (d.h., es verengt nicht wirklich bestehende Typen)
// %inferred-type: (number | null | undefined)[]
const mixedValues = [1, undefined, 2, null];
// %inferred-type: number[]
const numbers = mixedValues.filter(isNotNullish);
function isNotNullish<T>(value: T): value is NonNullable<T> { // (A)
return value !== undefined && value !== null;
}Leider müssen wir direkt eine Typ-Schutzfunktions-Funktion verwenden – eine Pfeilfunktion mit einer Typ-Schutzfunktion reicht nicht aus
// %inferred-type: (number | null | undefined)[]
const stillMixed1 = mixedValues.filter(
x => x !== undefined && x !== null);
// %inferred-type: (number | null | undefined)[]
const stillMixed2 = mixedValues.filter(
x => typeof x === 'number');TypeScript erlaubt uns, eigene Typ-Schutzfunktionen zu definieren – zum Beispiel
function isFunction(value: unknown): value is Function {
return typeof value === 'function';
}Der Rückgabetyp value is Function ist ein Typ-Prädikat. Er ist Teil der Typ-Signatur von isFunction()
// %inferred-type: (value: unknown) => value is Function
isFunction;Eine benutzerdefinierte Typ-Schutzfunktion muss immer Booleans zurückgeben. Wenn isFunction(x) true zurückgibt, verengt TypeScript den Typ des tatsächlichen Arguments x zu Function
function func(arg: unknown) {
if (isFunction(arg)) {
// %inferred-type: Function
arg; // type is narrowed
}
}Beachten Sie, dass TypeScript sich nicht darum kümmert, wie wir das Ergebnis einer benutzerdefinierten Typ-Schutzfunktion berechnen. Das gibt uns viel Freiheit bei den verwendeten Prüfungen. So hätten wir zum Beispiel isFunction() wie folgt implementieren können
function isFunction(value: any): value is Function {
try {
value(); // (A)
return true;
} catch {
return false;
}
}Leider müssen wir den Typ any für den Parameter value verwenden, da der Typ unknown den Funktionsaufruf in Zeile A nicht zulässt.
isArrayWithInstancesOf()/**
* This type guard for Arrays works similarly to `Array.isArray()`,
* but also checks if all Array elements are instances of `T`.
* As a consequence, the type of `arr` is narrowed to `Array<T>`
* if this function returns `true`.
*
* Warning: This type guard can make code unsafe – for example:
* We could use another reference to `arr` to add an element whose
* type is not `T`. Then `arr` doesn’t have the type `Array<T>`
* anymore.
*/
function isArrayWithInstancesOf<T>(
arr: any, Class: new (...args: any[])=>T)
: arr is Array<T>
{
if (!Array.isArray(arr)) {
return false;
}
if (!arr.every(elem => elem instanceof Class)) {
return false;
}
// %inferred-type: any[]
arr; // (A)
return true;
}In Zeile A sehen wir, dass der abgeleitete Typ von arr nicht Array<T> ist, aber unsere Prüfungen haben sichergestellt, dass es das derzeit ist. Deshalb können wir true zurückgeben. TypeScript vertraut uns und verengt zu Array<T>, wenn wir isArrayWithInstancesOf() verwenden
const value: unknown = {};
if (isArrayWithInstancesOf(value, RegExp)) {
// %inferred-type: RegExp[]
value;
}isTypeof()Dies ist ein erster Versuch, typeof in TypeScript zu implementieren
/**
* An implementation of the `typeof` operator.
*/
function isTypeof<T>(value: unknown, prim: T): value is T {
if (prim === null) {
return value === null;
}
return value !== null && (typeof prim) === (typeof value);
}Idealerweise könnten wir den erwarteten Typ von value über einen String angeben (d.h. eines der Ergebnisse von typeof). Dann müssten wir jedoch den Typ T aus diesem String ableiten, und es ist nicht sofort ersichtlich, wie das geht (es gibt einen Weg, wie wir bald sehen werden). Als Workaround geben wir T über ein Mitglied prim von T an
const value: unknown = {};
if (isTypeof(value, 123)) {
// %inferred-type: number
value;
}Eine bessere Lösung ist die Verwendung von Überladung (mehrere Fälle sind weggelassen)
/**
* A partial implementation of the `typeof` operator.
*/
function isTypeof(value: any, typeString: 'boolean'): value is boolean;
function isTypeof(value: any, typeString: 'number'): value is number;
function isTypeof(value: any, typeString: 'string'): value is string;
function isTypeof(value: any, typeString: string): boolean {
return typeof value === typeString;
}
const value: unknown = {};
if (isTypeof(value, 'boolean')) {
// %inferred-type: boolean
value;
}(Dieser Ansatz ist eine Idee von Nick Fisher.)
Eine Alternative ist die Verwendung einer Schnittstelle als Map von Strings zu Typen (mehrere Fälle sind weggelassen)
interface TypeMap {
boolean: boolean;
number: number;
string: string;
}
/**
* A partial implementation of the `typeof` operator.
*/
function isTypeof<T extends keyof TypeMap>(value: any, typeString: T)
: value is TypeMap[T] {
return typeof value === typeString;
}
const value: unknown = {};
if (isTypeof(value, 'string')) {
// %inferred-type: string
value;
}(Dieser Ansatz ist eine Idee von Ran Lottem.)
Eine Assertionsfunktion prüft, ob ihr Parameter bestimmte Kriterien erfüllt, und löst eine Exception aus, wenn dies nicht der Fall ist. Eine von vielen Sprachen unterstützte Assertionsfunktion ist beispielsweise assert(). assert(cond) löst eine Exception aus, wenn die boolesche Bedingung cond false ist.
Unter Node.js wird assert() über das eingebaute Modul assert unterstützt. Der folgende Code verwendet es in Zeile A
import assert from 'assert';
function removeFilenameExtension(filename: string) {
const dotIndex = filename.lastIndexOf('.');
assert(dotIndex >= 0); // (A)
return filename.slice(0, dotIndex);
}Die Typinferenz von TypeScript bietet spezielle Unterstützung für Assertionsfunktionen, wenn wir solche Funktionen mit Assertion-Signaturen als Rückgabetypen kennzeichnen. Bezüglich dessen, wie und was wir von einer Funktion zurückgeben können, ist eine Assertion-Signatur äquivalent zu void. Sie löst jedoch zusätzlich eine Verengung aus.
Es gibt zwei Arten von Assertion-Signaturen
asserts «cond»asserts «arg» is «type»asserts «cond»Im folgenden Beispiel besagt die Assertion-Signatur asserts condition, dass der Parameter condition true sein muss. Andernfalls wird eine Exception ausgelöst.
function assertTrue(condition: boolean): asserts condition {
if (!condition) {
throw new Error();
}
}So bewirkt assertTrue() eine Verengung
function func(value: unknown) {
assertTrue(value instanceof Set);
// %inferred-type: Set<any>
value;
}Wir verwenden das Argument value instanceof Set ähnlich wie eine Typ-Schutzfunktion, aber anstatt einen Teil einer bedingten Anweisung zu überspringen, löst false eine Exception aus.
asserts «arg» is «type»Im folgenden Beispiel besagt die Assertion-Signatur asserts value is number, dass der Parameter value den Typ number haben muss. Andernfalls wird eine Exception ausgelöst.
function assertIsNumber(value: any): asserts value is number {
if (typeof value !== 'number') {
throw new TypeError();
}
}Diesmal verengt der Aufruf der Assertionsfunktion den Typ ihres Arguments
function func(value: unknown) {
assertIsNumber(value);
// %inferred-type: number
value;
}Die Funktion addXY() fügt bestehenden Objekten Eigenschaften hinzu und aktualisiert deren Typen entsprechend
function addXY<T>(obj: T, x: number, y: number)
: asserts obj is (T & { x: number, y: number }) {
// Adding properties via = would be more complicated...
Object.assign(obj, {x, y});
}
const obj = { color: 'green' };
addXY(obj, 9, 4);
// %inferred-type: { color: string; } & { x: number; y: number; }
obj;Ein Schnittstellentyp S & T hat die Eigenschaften des Typs S und des Typs T.
function isString(value: unknown): value is string {
return typeof value === 'string';
}value is stringbooleanasserts «cond»function assertTrue(condition: boolean): asserts condition {
if (!condition) {
throw new Error(); // assertion error
}
}asserts conditionvoid, Exceptionasserts «arg» is «type»function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(); // assertion error
}
}asserts value is stringvoid, ExceptionEine Assertionsfunktion verengt den Typ eines bestehenden Werts. Eine erzwungene Konvertierungsfunktion gibt einen bestehenden Wert mit einem neuen Typ zurück – zum Beispiel
function forceNumber(value: unknown): number {
if (typeof value !== 'number') {
throw new TypeError();
}
return value;
}
const value1a: unknown = 123;
// %inferred-type: number
const value1b = forceNumber(value1a);
const value2: unknown = 'abc';
assert.throws(() => forceNumber(value2));Die entsprechende Assertionsfunktion sieht wie folgt aus
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') {
throw new TypeError();
}
}
const value1: unknown = 123;
assertIsNumber(value1);
// %inferred-type: number
value1;
const value2: unknown = 'abc';
assert.throws(() => assertIsNumber(value2));Erzwungene Konvertierung ist eine vielseitige Technik mit Anwendungen, die über die von Assertionsfunktionen hinausgehen. Zum Beispiel können wir konvertieren
Weitere Informationen finden Sie in [Inhalt nicht enthalten].
Betrachten Sie den folgenden Code
function getLengthOfValue(strMap: Map<string, string>, key: string)
: number {
if (strMap.has(key)) {
const value = strMap.get(key);
// %inferred-type: string | undefined
value; // before type check
// We know that value can’t be `undefined`
if (value === undefined) { // (A)
throw new Error();
}
// %inferred-type: string
value; // after type check
return value.length;
}
return -1;
}Anstelle der if-Anweisung, die in Zeile A beginnt, hätten wir auch eine Assertionsfunktion verwenden können
assertNotUndefined(value);Das Auslösen einer Exception ist eine schnelle Alternative, wenn wir keine solche Funktion schreiben möchten. Ähnlich wie beim Aufruf einer Assertionsfunktion aktualisiert diese Technik auch den statischen Typ.
@hqoss/guards: Bibliothek mit Typ-SchutzfunktionenDie Bibliothek @hqoss/guards bietet eine Sammlung von Typ-Schutzfunktionen für TypeScript – zum Beispiel
isBoolean(), isNumber() usw.isObject(), isNull(), isFunction() usw.isNonEmptyArray(), isInteger() usw.