Die Geschichte, wie man zwei Tage damit verbringt, denselben Code mehrmals neu zu schreiben.

Eintrag
In diesem Artikel werde ich Details zu Hapi, Joi, Routing und validate: { payload: ... }
weglassen validate: { payload: ... }
, was bedeutet, dass Sie bereits verstehen, worum es geht, sowie Terminologie, a la "Schnittstellen", "Typen" und dergleichen . Ich erzähle Ihnen nur von einer rundenbasierten, nicht der erfolgreichsten Strategie, meinem Training in diesen Dingen.
Ein kleiner Hintergrund
Jetzt bin ich der einzige Backend-Entwickler (der Code schreibt) für das Projekt. Funktionalität ist nicht die Essenz, aber die Schlüsselessenz ist ein ziemlich langes Profil mit persönlichen Daten. Die Geschwindigkeit und Qualität des Codes basiert auf meiner geringen Erfahrung, unabhängig an Projekten von Grund auf neu zu arbeiten, noch weniger Erfahrung mit JS (erst im 4. Monat) und auf dem Weg, sehr schief schräg, schreibe ich in TypeScript (im Folgenden - TS). Daten werden komprimiert, Rollen werden komprimiert, Änderungen kommen ständig an und es stellt sich heraus, dass zuerst Geschäftslogikcode und dann die Schnittstellen oben geschrieben werden. Trotzdem ist eine technische Aufgabe in der Lage, die Kappe einzuholen und darauf zu klopfen, was uns ungefähr passiert ist.
Nach dreimonatiger Arbeit an dem Projekt stimmte ich schließlich meinen Kollegen zu, zu einem einzigen Wörterbuch zu wechseln, damit die Eigenschaften des Objekts überall gleich benannt und geschrieben werden. In diesem Geschäft habe ich mich natürlich verpflichtet, eine Schnittstelle zu schreiben, und bin zwei Werktage lang fest damit verbunden.
Das Problem
Ein einfaches Benutzerprofil ist ein abstraktes Beispiel.
Zuerst Der Nullschritt eines guten Entwicklers: Daten beschreiben Tests schreiben;- Erster Schritt:
Tests schreiben Daten beschreiben - und so weiter.
Angenommen, für diesen Code wurden bereits Tests geschrieben, dann müssen die Daten noch beschrieben werden:
interface IUser { name: string; age: number; phone: string | number; } const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' };
Hier ist alles klar und sehr einfach. Wie wir uns erinnern, wird der gesamte Code im Backend oder besser gesagt in der API, dh der Benutzer, basierend auf Daten erstellt, die über das Netzwerk eingegangen sind. Daher müssen wir eingehende Daten validieren und Joi dabei helfen:
const joiUserValidator = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
Die Lösung "auf der Stirn" ist fertig. Das offensichtliche Minus dieses Ansatzes ist, dass der Validator vollständig von der Schnittstelle getrennt ist. Wenn sich die Felder während der Laufzeit der Anwendung ändern / hinzufügen oder sich ihr Typ ändert, muss diese Änderung manuell verfolgt und im Validator angezeigt werden. Ich denke, es wird keine so verantwortungsbewussten Entwickler geben, bis etwas fällt. Darüber hinaus besteht der Fragebogen in unserem Projekt aus mehr als 50 Feldern auf drei Verschachtelungsebenen, und es ist äußerst schwierig, dies zu verstehen, selbst wenn man alles auswendig kann.
Wir können const joiUserValidator: IUser
einfach nicht angeben, da Joi
seine Datentypen verwendet, die Fehler beim Kompilieren des Typs generieren. Der Type 'NumberSchema' is not assignable to type 'number'
. Aber muss es eine Möglichkeit geben, eine Validierung an der Schnittstelle durchzuführen?

Vielleicht habe ich es nicht richtig googelt oder die Antworten schlecht studiert, aber alle Entscheidungen waren darauf zurückzuführen, extractTypes
und eine Art heftiger Fahrräder wie diese zu extractTypes
:
type ValidatedValueType<T extends joi.Schema> = T extends joi.StringSchema ? string : T extends joi.NumberSchema ? number : T extends joi.BooleanSchema ? boolean : T extends joi.ObjectSchema ? ValidatedObjectType<T> : never;
Lösung
Verwenden Sie Bibliotheken von Drittanbietern
Warum nicht. Als ich Leute nach meiner Aufgabe fragte, erhielt ich in einer der Antworten und später und hier in den Kommentaren (dank Scharframmen ) Links zu diesen Bibliotheken:
https://github.com/typestack/class-validator
https://github.com/typestack/class-transformer
Es bestand jedoch ein Interesse, es selbst herauszufinden, die Arbeit von TS besser zu verstehen, und nichts drängte darauf, das Problem vorübergehend zu lösen.
Holen Sie sich alle Eigenschaften
Da ich bisher keine Arbeit mit Statik hatte, entdeckte der obige Code Amerika in Bezug auf die Verwendung ternärer Operatoren in Typen. Glücklicherweise war es nicht möglich, es im Projekt anzuwenden. Aber ich habe ein anderes interessantes Fahrrad gefunden:
interface IUser { name: string; age: number; phone: string | number; } type UserKeys<T> = { [key in keyof T]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
TypeScript
unter ziemlich kniffligen und mysteriösen Bedingungen können Sie beispielsweise Schlüssel von der Schnittstelle key in keyof T
, als wäre es ein normales JS-Objekt, jedoch nur key in keyof T
und durch key in keyof T
und nur durch Generika. Aufgrund des UserKeys
Typs sollten alle Objekte, die die Schnittstellen implementieren, dieselben Eigenschaften haben, die Wertetypen können jedoch beliebig sein. Dies enthält Hinweise in der IDE, gibt jedoch immer noch keinen klaren Hinweis auf die Wertetypen.
Hier ist ein weiterer interessanter Fall, den ich nicht verwenden konnte. Vielleicht können Sie mir sagen, warum dies notwendig ist (obwohl ich teilweise vermute, dass es nicht genug angewandte Beispiele gibt):
interface IUser { name: string; age: number; phone: string | number; } interface IUserJoi { name: Joi.StringSchema, age: Joi.NumberSchema, phone: Joi.AlternativesSchema } type UserKeys<T> = { [key in keyof T]: T[key]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const userJoiValidator: UserKeys<IUserJoi> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
Verwenden Sie Variablentypen
Sie können die Typen explizit festlegen und mithilfe von "ODER" und Extrahieren der Eigenschaften einen lokal funktionierenden Code abrufen:
type TString = string | Joi.StringSchema; type TNumber = number | Joi.NumberSchema; type TStdAlter = TString | TNumber; type TAlter = TStdAlter | Joi.AlternativesSchema; export interface IUser { name: TString; age: TNumber; phone: TAlter; } type UserKeys<T> = { [key in keyof T]; } const olex: UserKeys<IUser> = { name: 'Olex', age: 67, phone: '79998887766' }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
Das Problem dieses Codes tritt auf, wenn wir ein gültiges Objekt beispielsweise aus der Datenbank abrufen möchten, dh TS weiß nicht im Voraus, um welche Art von Daten es sich handelt - einfach oder Joi. Dies kann zu einem Fehler führen, wenn versucht wird, mathematische Operationen für ein Feld auszuführen, das als number
erwartet wird:
const someUser: IUser = getUserFromDB({ name: 'Aleg' }); const someWeirdMath = someUser.age % 10;
Dieser Fehler stammt von Joi.NumberSchema
da das Alter nicht nur eine number
. Wofür sie gekämpft haben und auf das sie gestoßen sind.
Zwei Lösungen zu einer kombinieren?
Irgendwann zu diesem Zeitpunkt näherte sich der Arbeitstag seinem logischen Abschluss. Ich holte Luft, trank Kaffee und löschte den verdammten Fick. Es ist notwendig, diese in Ihrem Internet weniger zu lesen! Die Zeit ist gekommen nimm eine Schrotflinte und Gehirnwäsche:
- Ein Objekt muss mit expliziten Werttypen gebildet werden.
- Sie können Generika verwenden, um Typen in eine einzelne Schnittstelle zu werfen.
- Generika unterstützen Standardtypen.
- Das
type
ist eindeutig zu etwas anderem fähig.
Wir schreiben die generische Schnittstelle mit Standardtypen:
interface IUser < TName = string, TAge = number, TAlt = string | number > { name: TName; age: TAge; phone: TAlt; }
Für Joi können Sie eine zweite Schnittstelle erstellen, die die Hauptschnittstelle auf folgende Weise erbt:
interface IUserJoi extends IUser < Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema > {}
Nicht gut genug, weil der nächste Entwickler IUserJoi
mit einem leichten Herzen oder schlechter erweitern kann. Eine eingeschränktere Option besteht darin, ein ähnliches Verhalten zu erzielen:
type IUserJoi = IUser<Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema>;
Wir versuchen:
const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; const joiUser: IUserJoi = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) };
UPD:
Um in Joi.object
ich mit dem Fehler TS2345
und die einfachste Lösung war as any
TS2345
. Ich denke, dies ist keine kritische Annahme, da sich das obige Objekt noch auf der Schnittstelle befindet.
const joiUserInfo = { info: Joi.object(joiUser as any).required() };
Es kompiliert, sieht am Verwendungsort ordentlich aus und setzt in Ermangelung besonderer Bedingungen immer die Standardtypen! Schönheit ...

... was ich zwei Arbeitstage verbracht habe
Zusammenfassung
Welche Schlussfolgerungen können daraus gezogen werden:
- Offensichtlich habe ich nicht gelernt, Antworten auf Fragen zu finden. Bei einer erfolgreichen Anfrage befindet sich diese Lösung (oder noch besser) in den ersten 5k-Links der Suchmaschine.
- Es ist nicht so einfach, von dynamischem zu statischem Denken zu wechseln. Viel häufiger hämmere ich nur auf ein solches Schwärmen.
- Generika sind cool. Auf Habr und Stackoverflow ist voll
von Fahrrädern Nicht offensichtliche Lösungen zum Erstellen einer starken Typisierung ... außerhalb der Laufzeit.
Was wir gewonnen haben:
- Beim Ändern der Schnittstelle fällt der gesamte Code ab, einschließlich des Validators.
- Im Editor wurden Tipps zu Eigenschaftsnamen und Arten von Objektwerten zum Schreiben eines Validators angezeigt.
- Das Fehlen obskurer Bibliotheken von Drittanbietern für denselben Zweck;
- Joi-Regeln werden nur angewendet, wenn dies erforderlich ist, in anderen Fällen Standardtypen.
- Wenn jemand den Werttyp einer Eigenschaft ändern möchte, wird er bei korrekter Organisation des Codes an den Ort gehen, an dem alle mit dieser Eigenschaft verknüpften Typen zusammengefasst sind.
- Wir haben gelernt, Generika wunderschön und einfach hinter der Typabstraktion zu verstecken und den Code visuell aus monströsen Konstruktionen zu entladen.
Moral: Erfahrung ist von unschätzbarem Wert, für den Rest gibt es eine Weltkarte.
Sie können das Endergebnis sehen, berühren und ausführen:
https://repl.it/@Melodyn/Joi-by-interface
