
Derzeit ist die Entwicklung einer modernen Front-End-Anwendung komplizierter als die hello world
Ebene, auf der das Team arbeitet (deren Zusammensetzung sich regelmäßig ändert), und stellt hohe Anforderungen an die Qualität der Codebasis. Um das Qualitätsniveau des Codes auf dem richtigen Niveau zu halten, halten wir uns im Front-End- Team #gostgroup auf dem Laufenden und haben keine Angst davor, moderne Technologien einzusetzen , die ihren praktischen Nutzen in Projekten von Unternehmen verschiedener Größenordnungen zeigen .
Die statistische Typisierung und ihre Vorteile am Beispiel von TypeScript wurden in verschiedenen Artikeln ausführlich diskutiert. Daher konzentrieren wir uns heute auf mehr angewandte Aufgaben, mit denen Front-End-Entwickler als Beispiel für den Lieblingsstapel unseres Teams (React + Redux) konfrontiert sind.
"Ich verstehe nicht, wie du im Allgemeinen ohne strenge Eingabe lebst. Was machst du? Tagelang belasten?" - Der Autor ist mir nicht bekannt.
"Nein, wir schreiben den ganzen Tag Typen" - mein Kollege.
Beim Schreiben von Code in TypeScript (im Folgenden wird der Betreffstapel impliziert) beklagen sich viele, dass sie viel Zeit damit verbringen müssen, Typen manuell zu schreiben. Ein gutes Beispiel für das Problem ist die connect
Connector-Funktion aus der react-redux
. Werfen wir einen Blick auf den folgenden Code:
type Props = { a: number, b: string; action1: (a: number) => void; action2: (b: string) => void; } class Component extends React.PureComponent<Props> { } connect( (state: RootStore) => ({ a: state.a, b: state.b, }), { action1, action2, }, )(Component);
Was ist das Problem hier? Das Problem ist, dass wir für jede neue Eigenschaft, die über den Connector eingefügt wird, den Typ dieser Eigenschaft im allgemeinen Typ der Komponenteneigenschaften (React) beschreiben müssen. Keine sehr interessante Beschäftigung, sagen Sie, Sie möchten immer noch in der Lage sein, die Art der Eigenschaften aus dem Konnektor in einer Art zu sammeln, die dann einmal mit der allgemeinen Art der Eigenschaften der Komponente "verbunden" wird. Ich habe gute Nachrichten für dich. Mit TypeScript können Sie dies heute tun! Bist du bereit Lass uns gehen!
Die Kraft von TypeScript
TypeScript steht nicht still und entwickelt sich ständig weiter (wofür ich es liebe). Ab Version 2.8 erschien darin eine sehr interessante Funktion (bedingte Typen), mit der Typen anhand von bedingten Ausdrücken zugeordnet werden können. Ich werde hier nicht auf Details eingehen, sondern nur einen Link zur Dokumentation hinterlassen und zur Veranschaulichung einen Code daraus einfügen:
type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; type T0 = TypeName<string>;
Wie diese Funktion in unserem Fall hilft. react-redux
der Beschreibung der InferableComponentEnhancerWithProps
react-redux
Bibliothekstypen finden Sie den InferableComponentEnhancerWithProps
Typ, der dafür verantwortlich ist, dass die Typen der injizierten Eigenschaften nicht in den externen Typ der Komponenteneigenschaften fallen, den wir beim Instanziieren der Komponente explizit festlegen müssen. Der Typ InferableComponentEnhancerWithProps
verfügt über zwei allgemeine Parameter: TInjectedProps
und TNeedsProps
. Wir interessieren uns für die erste. Versuchen wir, diesen Typ aus diesem Anschluss herauszuziehen!
type TypeOfConnect<T> = T extends InferableComponentEnhancerWithProps<infer Props, infer _> ? Props : never ;
Und ziehen Sie den Typ direkt aus einem realen Beispiel aus dem Repository (das Sie dort klonen und ein Testprogramm ausführen können):
import React from 'react'; import { connect } from 'react-redux'; import { RootStore, init, TypeOfConnect, thunkAction, unboxThunk } from 'src/redux'; const storeEnhancer = connect( (state: RootStore) => ({ ...state, }), { init, thunkAction: unboxThunk(thunkAction), } ); type AppProps = {} & TypeOfConnect<typeof storeEnhancer> ; class App extends React.PureComponent<AppProps> { componentDidMount() { this.props.init(); this.props.thunkAction(3000); } render() { return ( <> <div>{this.props.a}</div> <div>{this.props.b}</div> <div>{String(this.props.c)}</div> </> ); } } export default storeEnhancer(App);
Im obigen Beispiel teilen wir die Verbindung zum Repository (Redux) in zwei Stufen auf. In der ersten Phase erhalten wir einen KomponentenspeicherEnhancer höherer Ordnung (auch bekannt als InferableComponentEnhancerWithProps
), mit dem Sie mithilfe unseres TypeOfConnect
InferableComponentEnhancerWithProps
Eigenschaftstypen TypeOfConnect
und die erhaltenen Eigenschaftstypen (über den Schnittpunkt von &
types) mit nativen Eigenschaftstypen der Komponente weiter kombinieren können. In der zweiten Phase dekorieren wir einfach unsere Originalkomponente. Was auch immer Sie dem Connector hinzufügen, es fällt automatisch in die Komponenteneigenschaftstypen. Großartig, was wir erreichen wollten!
Ein aufmerksamer Leser bemerkte, dass Aktionsgeneratoren (der Kürze unboxThunk
vereinfachen wir den Begriff für Aktion) mit Nebenwirkungen (Thunk-Aktionsersteller) mithilfe der Funktion unboxThunk
einer zusätzlichen Verarbeitung unboxThunk
werden. Was hat eine solche zusätzliche Maßnahme verursacht? Lass es uns richtig machen. Schauen wir uns zunächst die Signatur einer solchen Aktion am Beispiel eines Programms aus dem Repository an:
const thunkAction = (delay: number): ThunkAction<void, RootStore, void, AnyAction> => (dispatch) => { console.log('waiting for', delay); setTimeout(() => { console.log('reset'); dispatch(reset()); }, delay); };
Wie Sie der Signatur entnehmen können, gibt unsere Aktion nicht sofort die Zielfunktion zurück, sondern zunächst eine Zwischenfunktion, die redux-middleware
aufnimmt, um Nebenwirkungen in unserer Hauptfunktion zu ermöglichen. Wenn Sie diese Funktion jedoch in der verbundenen Form in den Eigenschaften der Komponente verwenden, wird die Signatur dieser Funktion mit Ausnahme der Zwischenfunktion reduziert. Wie beschreibt man es in Typen? Benötigen Sie eine spezielle Konverterfunktion. Wieder zeigt TypeScript seine Kraft. Zunächst beschreiben wir den Typ, der die Zwischenfunktion aus der Signatur entfernt:
CutMiddleFunction<T> = T extends (...arg: infer Args) => (...args: any[]) => infer R ? (...arg: Args) => R : never ;
Hier verwenden wir zusätzlich zu bedingten Typen eine neuere Innovation aus TypeScript 3.0, mit der wir den Typ einer beliebigen Anzahl von Funktionsargumenten (Restparameter) ableiten können. Einzelheiten finden Sie in der Dokumentation . Jetzt bleibt es, den überschüssigen Teil auf ziemlich starre Weise aus unserer Handlung herauszuschneiden:
const unboxThunk = <Args extends any[], R, S, E, A extends Action<any>>( thunkFn: (...args: Args) => ThunkAction<R, S, E, A>, ) => ( thunkFn as any as CutMiddleFunction<typeof thunkFn> );
Wenn wir die Aktion durch einen solchen Konverter leiten, erhalten wir die Signatur, die wir am Ausgang benötigen. Jetzt kann die Aktion im Connector verwendet werden.
Durch einfache Manipulationen reduzieren wir unsere manuelle Arbeit beim Schreiben von typisiertem Code auf unseren Stapel. Wenn wir etwas weiter gehen, können wir auch die Eingabe von Aktionen und Reduzierern vereinfachen, wie wir es im Redux-Modus getan haben.
PS Wenn Sie die dynamische Bindung von Aktionen im Connector über die Funktion und redux.bindActionCreators
müssen Sie für eine korrektere Typisierung dieses Dienstprogramms sorgen (möglicherweise durch Schreiben eines eigenen Wrappers).
Update 0
Wenn jemand diese Lösung für praktisch hielt, kann sie Ihnen gefallen , sodass das Dienstprogramm type dem @types/react-redux
hinzugefügt wird.
Update 1
Einige weitere Typen, bei denen Sie den Typ der injizierten Requisiten des Sprunggelenks nicht explizit angeben müssen. Nehmen Sie einfach den Hoki und ziehen Sie die Typen heraus:
export type BasicHoc<T> = (Component: React.ComponentType<T>) => React.ComponentType<any>; export type ConfiguredHoc<T> = (...args: any[]) => (Component: React.ComponentType<T>) => React.ComponentType<any>; export type BasicHocProps<T> = T extends BasicHoc<infer Props> ? Props : never; export type ConfiguredHocProps<T> = T extends ConfiguredHoc<infer Props> ? Props : never; export type HocProps<T> = T extends BasicHoc<any> ? BasicHocProps<T> : T extends ConfiguredHoc<any> ? ConfiguredHocProps<T> : never ; const basicHoc = (Component: React.ComponentType<{a: number}>) => class extends React.Component {}; const configuredHoc = (opts: any) => (Component: React.ComponentType<{a: number}>) => class extends React.Component {}; type props1 = HocProps<typeof basicHoc>;
Update2
Der Typ aus dem Betreff befindet sich jetzt in @types/react-redux
( ConnectedProps ).