Hallo allerseits! Mein Name ist Dmitry Novikov, ich bin ein Javascript-Entwickler bei der Alfa Bank und heute werde ich Ihnen über unsere Erfahrungen bei der Ableitung des Aktionstyps mithilfe von Typescript berichten, welche Probleme wir hatten und wie wir sie gelöst haben.
Dies ist eine Abschrift meines Berichts über Alfa JavaScript MeetUp. Sie können den Code von den Präsentationsfolien
hier und die Aufzeichnung der Mitap-Sendung
hier sehen .
Unsere Front-End-Anwendungen laufen auf einer Reihe von React + Redux. Der Redux-Datenfluss sieht einfach so aus:

Es gibt Aktionsersteller - Funktionen, die eine Aktion zurückgeben. Aktionen fallen in den Reduzierer, der Reduzierer erzeugt eine neue Seite basierend auf der alten. Komponenten werden bei der Partei signiert, die wiederum neue Aktionen auslösen kann - und alles wiederholt sich.
So sieht der Aktionsersteller im Code aus:

Dies ist nur eine Funktion, die eine Aktion zurückgibt - ein Objekt, das ein Typzeichenfolgenfeld und einige Daten enthalten muss (optional).
So sieht ein typischer Reduzierer aus:

Dies ist ein regulärer Switch-Fall, der das Typfeld einer Aktion betrachtet und eine neue Seite generiert. Im obigen Beispiel werden einfach die Eigenschaftswerte der dortigen Aktion hinzugefügt.
Was ist, wenn wir versehentlich einen Fehler beim Schreiben eines Reduzierers machen? So werden wir beispielsweise die Eigenschaften verschiedener Aktionen austauschen:

Javascript weiß nichts über unsere Handlungen und betrachtet einen solchen Code als absolut gültig. Es wird jedoch nicht wie beabsichtigt funktionieren, und wir möchten diesen Fehler sehen. Was hilft uns, wenn nicht Typoskript? Versuchen wir, unsere Handlungen zu typisieren.

Zunächst schreiben wir für unsere Aktionen "Stirn" -Typen - Action1Type und Action2Type. Kombinieren Sie sie dann zu einem Verbindungstyp, um sie im Reduzierstück zu verwenden. Der Ansatz ist einfach und unkompliziert, aber was ist, wenn sich die Daten in den Aktionen während der Entwicklung der Anwendung ändern? Ändern Sie die Typen nicht jedes Mal manuell. Wir schreiben sie wie folgt um:

Der Operator typeof gibt den Typ des Aktionserstellers an uns zurück, und ReturnType gibt uns den Typ des Rückgabewerts der Funktion an - d. H. Art der Aktion. Infolgedessen wird es genauso wie auf der obigen Folie angezeigt, jedoch nicht mehr manuell. Wenn Sie Aktionen ändern, werden die Aktionstypen vom Unionstyp automatisch aktualisiert. Wow! Wir schreiben es in den Reduzierer und ...

Und sofort bekommen wir Fehler aus dem Skript. Darüber hinaus sind die Fehler nicht ganz klar - die Balkeneigenschaft fehlt in der foo-Aktion und foo fehlt in der Bar ... Es scheint so zu sein, wie es sein sollte? Etwas scheint durcheinander zu sein. Im Allgemeinen funktioniert der Stirnansatz nicht wie erwartet.
Dies ist jedoch nicht das einzige Problem. Stellen Sie sich vor, dass unsere Anwendung im Laufe der Zeit wächst und wir viele Aktionen ausführen werden. Sehr viel.

Wie würde unser üblicher Typ in diesem Fall für sie aussehen? Wahrscheinlich so etwas:

Und wenn wir berücksichtigen, dass die Aktionen hinzugefügt und gelöscht werden, müssen wir dies alles manuell unterstützen - Typen hinzufügen und löschen. Das passt uns auch gar nicht. Was zu tun ist? Beginnen wir mit dem ersten Problem.

Wir haben also ein paar Aktionsersteller, und der gemeinsame Typ für sie ist die Vereinigung automatisch abgeleiteter Aktionstypen. Jede Aktion verfügt über eine type-Eigenschaft und ist als Zeichenfolge definiert. Dies ist die Wurzel des Problems. Um eine Aktion von einer anderen zu unterscheiden, muss jeder Typ eindeutig sein und nur einen eindeutigen Wert akzeptieren.

Dieser Typ wird als Literal bezeichnet. Es gibt drei Arten von Literalen: numerisch, string und boolean.

Zum Beispiel haben wir den Typ onlyNumberOne und geben an, dass eine Variable dieses Typs nur der Zahl 1 entsprechen kann. Weisen Sie 2 zu - und erhalten Sie einen Typoskriptfehler. String funktioniert ähnlich - einer Variablen kann nur ein bestimmter String-Wert zugewiesen werden. Nun, Boolescher Wert ist entweder wahr oder falsch, ohne Mehrdeutigkeit.
Generisch
Wie kann dieser Typ gespeichert werden, ohne dass daraus eine Zeichenfolge wird? Wir werden Generika verwenden. Generisch ist eine solche Abstraktion über Typen. Angenommen, wir haben eine nutzlose Funktion, die eine Eingabe als Argument verwendet und ohne Änderungen zurückgibt. Wie kann ich es eingeben? Schreiben Sie welche, weil es absolut jeder Typ sein kann? Wenn jedoch eine Art Logik in der Funktion vorhanden ist, kann eine Typkonvertierung erfolgen, und beispielsweise kann eine Zahl in eine Zeichenfolge umgewandelt werden, und jede beliebige Kombination überspringt dies. Ungeeignet.

Ein Generikum hilft uns, aus dieser Situation herauszukommen. Der obige Eintrag bedeutet, dass wir ein Argument eines bestimmten Typs T übergeben und die Funktion genau den gleichen Typ T zurückgibt. Wir wissen nicht, um welches es sich handelt - eine Zahl, eine Zeichenfolge, ein Boolescher Wert oder etwas anderes -, aber wir können dies garantieren es wird genau der gleiche Typ sein. Diese Option passt zu uns.
Lassen Sie uns das Konzept der Generika ein wenig entwickeln. Wir müssen nicht alle Typen im Allgemeinen verarbeiten, sondern ein konkretes String-Literal. Hierfür gibt es ein erweitertes Schlüsselwort:

Die Notation "T erweitert Zeichenfolge" bedeutet, dass T ein bestimmter Typ ist, der eine Teilmenge des Zeichenfolgentyps ist. Es ist erwähnenswert, dass dies nur mit primitiven Typen funktioniert. Wenn wir anstelle eines Strings einen Objekttyp mit einem bestimmten Satz von Eigenschaften verwenden würden, würde dies im Gegenteil bedeuten, dass T ein OVER-Satz dieses Typs ist.
Im Folgenden finden Sie Beispiele für die Verwendung einer mit Extend und Generics typisierten Funktion:

- Argument vom Typ string - Die Funktion gibt einen String zurück
- Ein Argument vom Typ Literal String - Die Funktion gibt einen Literal String zurück
- Wenn das Argument nicht wie eine Zeichenfolge aussieht, z. B. eine Zahl oder ein Array, gibt das Skript einen Fehler aus.
Nun, und insgesamt funktioniert es.

Wir ersetzen unsere Funktion durch den Typ der Aktion - sie gibt genau den gleichen Zeichenfolgentyp zurück, aber nur ist es keine Zeichenfolge mehr, sondern eine Literalzeichenfolge, wie sie sein sollte. Wir sammeln Gewerkschaftstypen, wir kennzeichnen einen Reduzierer - alles ist in Ordnung. Und wenn wir einen Fehler machen und die falschen Eigenschaften schreiben, gibt uns das Zeitskript nicht zwei, sondern einen logischen und verständlichen Fehler:

Gehen wir etwas weiter und abstrahieren vom String-Typ. Wir werden dieselbe Typisierung schreiben, nur mit zwei Generika - T und U. Jetzt haben wir einen bestimmten Typ von T, der von einem anderen Typ von U abhängt, anstelle dessen wir alles verwenden können - mindestens Zeichenfolge, mindestens Zahl, mindestens Boolescher Wert. Dies wird mit der Wrapper-Funktion implementiert:

Und schließlich: Das beschriebene Problem hing lange Zeit als Problem auf dem Github, und schließlich präsentierten uns die Entwickler in Typescript Version 3.4 eine Lösung - const Assertion. Es gibt zwei Arten der Aufnahme:

Wenn Sie also ein neues Typoskript haben, können Sie entweder als const in den Aktionen verwenden, und der Literaltyp wird nicht zu einer Zeichenfolge. In älteren Versionen können Sie die oben beschriebene Methode verwenden. Es stellt sich heraus, dass wir jetzt bis zu zwei Lösungen für das erste Problem haben. Aber der zweite bleibt.

Wir haben immer noch viele verschiedene Aktionen, und obwohl wir jetzt wissen, wie wir mit ihren Typen richtig umgehen sollen, wissen wir immer noch nicht, wie wir sie automatisch zusammensetzen sollen. Wir können Union manuell schreiben, aber wenn Aktionen gelöscht und hinzugefügt werden, müssen wir sie weiterhin manuell löschen und dem Typ hinzufügen. Das ist nicht richtig.

Wo soll ich anfangen? Angenommen, wir haben Aktionsersteller zusammen aus einer einzelnen Datei importiert. Wir möchten sie einzeln umgehen, die Arten ihrer Handlungen ableiten und sie zu einem Gewerkschaftstyp zusammenfassen. Und vor allem möchten wir dies automatisch tun, ohne die Typen manuell zu bearbeiten.

Beginnen wir mit den Action-Erstellern. Zu diesem Zweck gibt es einen speziellen zugeordneten Typ, der die Schlüsselwertsammlungen beschreibt. Hier ist ein Beispiel:

Dadurch wird ein Typ für ein Objekt erstellt, dessen Schlüssel Option1 und Option2 (aus dem Schlüsselsatz) sind und deren Werte wahr oder falsch sind. In einer allgemeineren Version kann dies als eine Art mapOfBool dargestellt werden - ein Objekt mit einer Art Zeilenschlüssel und booleschen Werten.
Gut. Aber wie können wir überprüfen, ob es sich um ein Objekt handelt, das uns bei der Eingabe übergeben wird, und nicht um einen anderen Typ? Der bedingte Typ, ein einfacher Ternär in der Welt der Typen, wird uns dabei helfen.

In diesem Beispiel überprüfen wir: Typ T hat etwas mit String gemeinsam? Wenn ja, geben Sie einen String zurück, und wenn nicht, geben Sie nie zurück. Dies ist ein so spezieller Typ, der uns immer einen Fehler zurückgibt. String-Literal erfüllt die ternäre Bedingung. Hier sind einige Beispielcodes:

Wenn wir in den Generika etwas angeben, das nicht wie eine Zeichenfolge ist, gibt uns Typoskript einen Fehler.
Wir haben die Problemumgehung und Überprüfung herausgefunden. Es bleibt nur, die Typen zu erhalten und sie zu einer Vereinigung zusammenzuführen. Dies hilft uns bei der Inferenz von Typinferenzen im Typoskript. Infer lebt normalerweise in einem bedingten Typ und macht so etwas: Es durchläuft alle Schlüssel-Wert-Paare, versucht, den Werttyp abzuleiten und vergleicht ihn mit den anderen. Wenn die Wertetypen unterschiedlich sind, werden sie zu einer Vereinigung zusammengefasst. Genau das, was wir brauchen!

Nun bleibt es, alles zusammenzusetzen.
Es stellt sich heraus, dieses Design:

Die Logik lautet ungefähr wie folgt: Wenn T wie ein Objekt aussieht, das einige Zeichenfolgenschlüssel (Namen von Aktionserstellern) und Werte eines bestimmten Typs (eine Funktion, die die Aktion an uns zurückgibt) hat, versuchen Sie, diese Paare zu umgehen, und leiten Sie den Typ dieser Werte ab und reduzieren ihren gemeinsamen Typ. Und wenn etwas schief geht - werfen Sie einen speziellen Fehler aus (geben Sie nie ein).
Es ist nur auf den ersten Blick schwierig. In der Tat ist alles ganz einfach. Es lohnt sich, auf ein interessantes Merkmal zu achten - aufgrund der Tatsache, dass jede Aktion ein eindeutiges Typfeld hat, bleiben die Typen dieser Aktionen nicht zusammen und wir erhalten einen vollständigen Vereinigungstyp am Ausgang. So sieht es im Code aus:

Wir importieren die Aktionsersteller als Aktionen, nehmen ihren ReturnType (der Typ des Rückgabewerts sind Aktionen) und sammeln mit unserem speziellen Typ. Es stellt sich heraus, was genau erforderlich war.

Was ist das Ergebnis? Wir haben Vereinigung von wörtlichen Typen für alle Handlungen. Wenn eine neue Aktion hinzugefügt wird, wird der Typ automatisch aktualisiert. Infolgedessen erhalten wir eine vollwertige, strenge Typisierung von Aktionen. Jetzt können wir keinen Fehler mehr machen. Auf dem Weg dorthin haben wir etwas über Generika, bedingte Typen, zugeordnete Typen, Nie und Schlussfolgerungen gelernt. Weitere Informationen zu diesen Tools erhalten Sie
hier .