Wie schreibe ich einen "isX" -Typschutz für eine generische Typliste <T>, ohne den Typ von T zu kennen?


Jamie Ridding

Ich schreibe eine typisierte Listenimplementierung in TypeScript als einfaches Objekt, dessen Struktur durch einen typeAusdruck sichergestellt wird . Ich habe mich dafür entschieden, es als einfaches Objekt anstelle einer Klasse zu schreiben, da ich glaube, dass ein einfaches Objekt es mir leichter machen würde, seine Unveränderlichkeit sicherzustellen. Die Typdefinition dieses Objekts lautet wie folgt:

type List<T> = Readonly<{
  head: T;
  tail: List<T> | void;
}>;

Ich möchte einen Typschutz für diese Struktur schreiben, damit ich in anderem Code sicherstellen kann, dass ich an einem ListObjekt arbeite. Mein aktueller Typschutz sieht wie folgt aus:

export function isList(list: any): list is List {
  if (isNil(list)) {
    return false;
  }

  return ("head" in list && "tail" in list);
}

Diese Implementierung weist in der ersten Zeile einen Fehler auf list is List: TypeScript beschwert sich darüber, dass der Typ List<T>generisch ist, und als solcher muss ich im Verweis darauf einen Typ angeben.

Die erwartete Ausgabe dieser Funktion erfolgt wie folgt:

import {getListSomehow, isList} from "list";

const list = getListSomehow("hello", "world", "etc...");
const arr = ["hello", "world", "etc..."];

console.log(isList(list)); // true
console.log(isList(arr)); // false
console.log(isList("a value")); // false

Gibt es eine Möglichkeit, diesen Typ Guard zu schreiben, ohne den Typ kennen zu müssen T? Wenn nicht, gibt es eine Möglichkeit, diesen Typ irgendwie abzurufen, während die Funktion dennoch einen beliebigen Wert annehmen kann?

Zenexer

Diese Frage enthüllt einige der Mängel von TypeScript. Die kurze Antwort lautet: Sie können wahrscheinlich nicht das tun, was Sie versuchen, zumindest nicht so, wie Sie es versuchen. (Wir haben dies in den Kommentaren zur Frage ein wenig besprochen.)

Aus Gründen der Typensicherheit stützt sich TypeScript normalerweise auf die Typprüfung zur Kompilierungszeit. Im Gegensatz zu JavaScript haben TypeScript-Bezeichner einen Typ. manchmal wird dies explizit angegeben, manchmal wird es vom Compiler abgeleitet. Wenn Sie versuchen, einen Bezeichner als einen Typ zu behandeln, der sich von seinem bekannten Typ unterscheidet, beschwert sich der Compiler im Allgemeinen.

Dies stellt ein Problem für die Schnittstelle zu vorhandenen JavaScript-Bibliotheken dar. Bezeichner in JavaScript haben keine Typen. Darüber hinaus ist es nicht möglich, den Typ eines Werts zur Laufzeit zuverlässig zu überprüfen.

Dies führte zum Aufkommen von Typwächtern. Es ist möglich, eine Funktion in TypeScript zu schreiben, deren Zweck es ist, dem Compiler mitzuteilen, dass eines der an sie übergebenen Argumente ein bestimmter Typ ist, wenn die Funktion true zurückgibt. Auf diese Weise können Sie Ihre eigene Ententypisierung als eine Art Verbindung zwischen JavaScript und TypeScript implementieren. Eine Typschutzfunktion sieht ungefähr so ​​aus:

const isDuck = (x: any): x is Duck => looksLikeADuck(x) && quacksLikeADuck(x);

Dies ist keine großartige Lösung, funktioniert jedoch, solange Sie sorgfältig darauf achten, wie Sie den Typ überprüfen, und es gibt keine wirklichen Alternativen.

Typschutz funktioniert jedoch nicht gut für generische Typen. Denken Sie daran, dass der Zweck des Typschutzes darin besteht, eine anyEingabe vorzunehmen und festzustellen, ob es sich um einen bestimmten Typ handelt. Wir können mit generischen Typen einen Weg dorthin finden:

function isList(list: any): list is List<any> {
    if (isNil(list)) {
        return false;
    }

    return "head" in list && "tail" in list;
}

Dies ist jedoch immer noch nicht ideal. Wir können jetzt testen, ob etwas a ist List<any>, aber wir können nicht auf etwas Spezifischeres List<number>testen - und ohne das wird das Ergebnis für uns wahrscheinlich nicht besonders nützlich sein, da alle unsere gekapselten Werte noch von unbekanntem Typ sind .

Was wir wirklich wollen, ist etwas, das überprüfen kann, ob etwas ein ist List<T>. Das wird etwas kniffliger:

function isList<T>(list: any): list is List<T> {
    if (isNil(list)) {
        return false;
    }

    if (!("head" in list && "tail" in list)) {
        return false;
    }

    return isT<T>(list.head) && (isNil(list.tail) || isList<T>(list.tail));
}

Jetzt müssen wir definieren isT<T>:

function isT<T>(x: any): x is T {
    // What goes here?
}

Das können wir aber nicht. Wir können zur Laufzeit nicht überprüfen, ob ein Wert ein beliebiger Typ ist. Wir können uns irgendwie herumhacken:

function isList<T>(list: any, isT: (any) => x is T): list is List<T> {
    if (isNil(list)) {
        return false;
    }

    if (!("head" in list && "tail" in list)) {
        return false;
    }

    return isT(list.head) && (isNil(list.tail) || isList<T>(list.tail, isT));
}

Jetzt ist es das Problem des Anrufers:

function isListOfNumbers(list: any): list is List<number> {
    return isList<number>(list, (x): x is number => typeof x === "number");
}

Nichts davon ist ideal. Wenn Sie es vermeiden können, sollten Sie; Verlassen Sie sich stattdessen auf die strenge Typprüfung von TypeScript. Ich habe Beispiele geliefert, aber zuerst müssen wir die Definition von List<T>: anpassen.

type List<T> = Readonly<null | {
    head: T;
    tail: List<T>;
}>;

Nun, mit dieser Definition, anstatt:

function sum(list: any) {
    if (!isList<number>(list, (x): x is number => typeof x === "number")) {
        throw "Panic!";
    }

    // list is now a List<number>--unless we wrote our isList or isT implementations incorrectly.

    let result = 0;

    for (let x = list; x !== null; x = x.tail) {
        result += list.head;
    }

    return result;
}

Verwenden:

function sum(list: List<number>) {
    // list is a List<number>--unless someone called this function directly from JavaScript.

    let result = 0;

    for (let x = list; x !== null; x = x.tail) {
        result += list.head;
    }

    return result;
}

Wenn Sie eine Bibliothek schreiben oder sich mit einfachem JavaScript beschäftigen, haben Sie möglicherweise nicht überall den Luxus einer strengen Typprüfung, aber Sie sollten Ihr Bestes tun, um sich darauf zu verlassen, wenn Sie können.

Verwandte Artikel