Redux: Unterstützung für typisierte Aktionen (Typescript)

Erstellt am 2. Nov. 2015  ·  71Kommentare  ·  Quelle: reduxjs/redux

Bezogen auf diese beiden Probleme:

Hallo Dan,

Die Firma, bei der ich arbeite, verwendet sowohl Redux als auch Typescript, und wir möchten unseren Aktionen einige zusätzliche Garantien für die Kompilierungszeit hinzufügen. Derzeit ist dies aufgrund der Einschränkung, dass Aktionen einfache Objekte sein müssen, nicht möglich. Sie sagten, dass dies daran liegt, dass "Aufzeichnen/Wiedergeben und zugehörige Funktionen mit einer Aktionsklasse nicht funktionieren" und ich verstehe, dass dies wertvolle Funktionen sind und ich sie nicht verlieren möchte. Aber ich hatte Code, der Typescript-Klassen verwendet, die (wenn die Redux-Devtools aktiviert waren) immer noch Aktionen aufzeichnen, abbrechen und wiedergeben konnten (mit dem Code, den ich in Ausgabe 355 gepostet habe).

Wenn ich die Devtools entferne, bekomme ich nur Fehler, die überprüfen, ob Aktionen einfache Objekte sind und die Ausführung dort aufhört. Mir scheint es zu streng zu sein, mehr zu überprüfen, als wirklich erforderlich ist. Ich hätte viel lieber eine Überprüfung, die erfordert, dass die Aktionen serialisierbar sind. Ich weiß, dass die Leistung ein Problem ist - IMO sollte sie nicht garantieren / beweisen müssen, dass sie serialisierbar sind. Überprüfen Sie einfach, ob die Aktionsobjekte selbst dies angeben, und vertrauen Sie ihnen dann.
(Ich bin mir nicht sicher, warum die Prüfung nicht auch fehlschlägt, wenn Devtools aktiviert sind - vielleicht können Sie das beleuchten).

Ich würde wirklich gerne die Vorteile von Typescript und Redux nutzen können. Ich hoffe, Sie können mir (und anderen, die diese beiden Tools verwenden) helfen, eine Lösung zu finden.

Ich möchte auch wiederholen, dass (so sehr ich es mir auch anders wünschte!) Typoskript keine diskriminierten Gewerkschaften hat, daher werden Lösungen, die auf diesem Ansatz basieren, nicht funktionieren.

Danke schön!

discussion

Hilfreichster Kommentar

Ich verwende FSA und fand es sehr praktisch, eine generische Action Schnittstelle mit einem Typparameter zu haben, der dem Typ payload :

interface Action<T>{
  type: string;
  payload: T;
  error?: boolean;
  meta?: any;
}

Dann exportiere ich den Nutzlasttyp neben dem Aktionstyp mit demselben Namen:

export const ADD_TODO = 'ADD_TODO';
export type ADD_TODO = {text: string};

Auf diese Weise importieren Sie beim Importieren von ADD_TODO tatsächlich sowohl den Aktionstyp als auch den Nutzlasttyp:

import {ADD_TODO} from './actions';
import {handleActions} from 'redux-actions';

const reducer = handleActions({
  [ADD_TODO]: (state, action:Action<ADD_TODO>) => {
    // enabled type checking for action.payload
    // ...
  }
});

Alle 71 Kommentare

Ermöglicht TypeScript die Typprüfung von Formen von einfachen Objekten, anstatt Klassen zu definieren?
Mein einziges Problem besteht darin, Instanzen von Klassen für Aktionen zu verwenden.

Würden TypeScript-Schnittstellen nicht ausreichen, um einfache

Fast - das Problem dabei ist, dass sie nur zur Kompilierzeit existieren. Der Reducer hat also keine Möglichkeit zu erkennen, welche eingehende Aktion von welcher Schnittstelle stammt. ...Es sei denn, ich behalte die 'type'-Eigenschaft für jede Aktion bei und setze sie dann manuell um, nachdem ich diese Eigenschaft überprüft habe. Und an diesem Punkt erhalte ich keine zusätzliche Typsicherheit, wenn ich sie nur als einfache Objekte behandle.

@nickknw Nun, es kann keine Möglichkeit geben, diese Aktionen stark zu tippen! Ich meine, es wäre nicht möglich, selbst wenn es C# wäre! So ist die Architektur, das hat nichts mit Redux zu tun. Ich selbst benutze Redux mit Typoskript auch. Und das Beste, was ich mir einfallen ließ, war, Interfaces mit demselben Namen wie meine actionTypes zu haben, die Interfaces und Konstanten an derselben Stelle zu belassen, meine actionCreators dazu zu bringen, die Typisierungen zu respektieren und in die Reducer zu konvertieren. Es ist nicht so schwer zu warten und gibt mir die Typsicherheit, die ich brauche, mit klaren Absichten, warum die Schnittstellen existieren. Ich würde dir vorschlagen, diesen Ansatz in Betracht zu ziehen, er hat für mich funktioniert :grin: und mein Projekt ist irgendwie riesig :D

Sie sagten, dass dies daran liegt, dass "Aufzeichnen/Wiedergeben und zugehörige Funktionen mit einer Aktionsklasse nicht funktionieren" und ich verstehe, dass dies wertvolle Funktionen sind und ich sie nicht verlieren möchte. Aber ich hatte Code, der Typescript-Klassen verwendet, die (wenn die Redux-Devtools aktiviert waren) immer noch Aktionen aufzeichnen, abbrechen und wiedergeben konnten (mit dem Code, den ich in Ausgabe 355 gepostet habe).

Ja, aber wenn Sie den persistState() Enhancer verwenden, sie serialisieren und später Ihren Zustand wiederherstellen und deserialisieren, werden sie nicht mehr funktionieren, da Ihr Code auf instanceof Prüfungen angewiesen ist.

Die „einfachen Objekte“ sind nicht wirklich der Punkt. Was wir durchsetzen möchten, ist, dass Sie sich nicht darauf verlassen, dass Aktionen Instanzen bestimmter Klassen in Ihren Reduzierern sind, da dies nach der (De-)Serialisierung nie der Fall sein wird.

Mit anderen Worten, dies ist ein Anti-Muster:

function reducer(state, action) {
  if (action instanceof SomeAction) {
    return ...
  } else {
    return ...
  }
}

Wenn wir es TypeScript-Benutzern ermöglichen, weiß ich, dass JavaScript-Benutzer versucht sein werden und dies missbrauchen werden, weil viele Leute, die aus traditionellen OO-Sprachen kommen, daran gewöhnt sind, Code auf diese Weise zu schreiben. Dann verlieren sie die Vorteile der Aufnahme/Wiedergabe in Redux.

Einer der Punkte von Redux besteht darin, einige Einschränkungen durchzusetzen, die wir für wichtig halten und die, wenn sie nicht durchgesetzt werden, später keine umfangreichen Tools ermöglichen. Wenn wir den Leuten erlauben, Klassen für Aktionen zu verwenden, werden sie dies tun, und es wird unmöglich sein, etwas wie redux-recorder zu implementieren, weil es keine Garantie gibt, dass Leute einfache Objektaktionen verwenden. Vor allem, wenn sich Boilerplate-Projekte plötzlich dazu entschließen, Kurse aufzunehmen, weil „wow, jetzt ist es erlaubt, es muss eine großartige Idee sein“.

Was wir durchsetzen möchten, ist, dass Sie sich nicht darauf verlassen, dass Aktionen Instanzen bestimmter Klassen in Ihren Reduzierern sind, da dies nach der (De-)Serialisierung nie der Fall sein wird.

Ich glaube, hier hast du mich verloren. Ich verstehe nicht, warum das Serialisieren und Deserialisieren eines Objekts notwendigerweise bedeutet, dass es seine klassenbezogenen Informationen verliert.

let actions = [new SomeAction(), new OtherAction()];
let json = JSON.stringify(actions);
let revivedActions = JSON.parse(json);
// reducers no longer understand revivedActions because type info is stripped.

Ein weiterer Grund, warum ich nicht zulassen möchte, dass die Leute Klassen für Aktionen erstellen, ist, dass dies die Leute dazu ermutigen wird

a) Erstellen Sie Klassenhierarchien
b) Fügen Sie dort Logik ein, z. B. „Patch“-Methode von https://github.com/rackt/redux/issues/437#issue -99905895

Beides widerspricht meiner Absicht, Redux zu verwenden. React hat jedoch gezeigt, dass, wenn Sie den Leuten ES6-Klassen geben, sie die Vererbung verwenden, selbst wenn es viel bessere Alternativen gibt, nur weil sie so in der Softwareentwicklung verankert ist. Es ist schwer, alte Gewohnheiten aufzugeben, es sei denn, man wird dazu gezwungen.

Es ist traurig, dass TypeScript Sie dazu zwingt, Dinge, die Sie stark eingeben möchten, in Klassen umzuwandeln, aber ich glaube, dass dies ein Fehler von TypeScript ist, nicht unserer. Wie in https://github.com/rackt/redux/issues/992#issuecomment -153138035 beschrieben, halte ich Casting für die beste Lösung, die wir anbieten können. Sie können auch ein redux-typescript-store Projekt mit createStore() , das diese Einschränkung nicht hat, aber ich bin immer noch nicht gezwungen, es zu entfernen.

Schließlich: fürs Protokoll, ich hasse den Unterricht nicht.
Das ist nicht mein Punkt.

Mein einziger Punkt ist, dass Handlungen _passive Beschreibungen dessen sein sollten, was passiert ist_. Wenn wir zulassen, dass sie Klasseninstanzen sind, bauen die Leute seltsame Abstraktionen auf Aktionen mit Klassen und leiden darunter.

Nicht wollen.

Ahh okay, danke für das Beispiel. Ich wusste nicht, dass JSON.stringify Prototypinformationen verwirft. Im Nachhinein macht es für mich Sinn, dies zu tun, angesichts der Grenzen dessen, was JSON speichern kann.

Einer der Punkte von Redux besteht darin, einige Einschränkungen durchzusetzen, die wir für wichtig halten und die, wenn sie nicht durchgesetzt werden, später keine umfangreichen Tools ermöglichen.

Ja, das verstehe ich, bin dabei und möchte keine Kompromisse bei den Garantien eingehen, auf die sich die Tools verlassen können. Ich möchte keine Klassenhierarchien erstellen oder Logik an Klassen anhängen, sondern sie nur zur Kompilierzeit unterscheiden können.

Es ist traurig, dass TypeScript Sie dazu zwingt, Dinge, die Sie stark eingeben möchten, in Klassen umzuwandeln, aber ich glaube, dass dies ein Fehler von TypeScript ist, nicht unserer.

Schnittstellen funktionieren gut genug für das, wofür sie entwickelt wurden, aber ich stimme zu, dass es für diesen Anwendungsfall bedauerlich ist, dass sie zur Laufzeit keine Typinformationen bereithalten (oder eine Laufzeitprüfung ermöglichen, ob dies mit dieser Schnittstelle übereinstimmt? Weg).

Mein einziger Punkt ist, dass Handlungen passive Beschreibungen dessen sein sollten, was passiert ist.

Hier stimme dir zu 100% zu

Wenn wir sie als Klasseninstanzen zulassen, bauen die Leute seltsame Abstraktionen auf Aktionen mit Klassen auf und leiden darunter.

Puh, das stimmt wohl auch.


Ich denke immer noch, dass es hier Raum für einen Mittelweg geben könnte, mit einer etwas flexibleren Art, Aktionen zu serialisieren. Es gibt die Parameter 'reviver' und 'replacer' für Parsen und Stringify - vielleicht gibt es eine Möglichkeit, eine Serialisierungsfunktion zu erstellen, die genug Informationen für mich behält, um einige grundlegende Typinformationen weiterzugeben, aber auch genug über die Struktur weiß, um Warnungen / Fehler zu machen über die Szenarien, die Sie verhindern möchten.

Ich würde lieber nicht den Weg des manuellen Castings gehen - ich könnte, wenn nichts anderes funktioniert, aber an diesem Punkt bringen die Typen keinen großen Mehrwert für den Aufwand.

Und ja, es wäre schön, wenn Typescript flexiblere Eingabeoptionen hätte, damit ich keine Klassen verwenden müsste, aber das ist etwas, was ich realistischerweise nicht beeinflussen oder in naher Zukunft ändern kann.

Vielleicht gibt es eine Möglichkeit, eine Serialisierungsfunktion zu erstellen, die genügend Informationen speichert, damit ich einige grundlegende Typinformationen weitergeben kann, aber auch genug über die Struktur weiß, um Warnungen/Fehler über die Szenarien zu vermeiden, die Sie verhindern möchten.

Dies würde bedeuten, dass Benutzer Funktionen implementieren, die type Strings auf Klassen abbilden, was wiederum ziemlich manuelles Casting ist. Und wir wollen nicht, dass Benutzer diese Art von Sachen implementieren, um Aufnahme/Wiedergabe zum Laufen zu bringen. Dies ist ein weiterer Punkt von Redux: Sie erhalten Hot-Reloading, Aufnahme/Wiedergabe _kostenlos_, ohne serialize() , hydrate() usw. zu implementieren.

Dies würde bedeuten, dass Benutzer Funktionen implementieren, die Typ-Strings auf Klassen abbilden, was ziemlich viel manuelles Umwandeln ist.

Im Idealfall würde es nur die Implementierung eines Paars von Funktionen beinhalten, die dies tun, und dann würden alle Aktionsklassen so implementiert, dass diese Funktionen sie serialisieren könnten. Anstatt menschliches Versagen möglicherweise einmal für jede abgewickelte Aktion einzuführen, wäre es nur einmal.

Und wir wollen nicht, dass Benutzer diese Art von Sachen implementieren, um Aufnahme/Wiedergabe zum Laufen zu bringen.

Nicht das, was ich vorgeschlagen habe - dies könnte etwas sein, das opt-in ist. Wenn Sie es nicht tun müssen, funktioniert alles wie gewohnt. Ich verstehe, dass Sie vielleicht nicht begeistert davon sind, diese Dose Würmer zu öffnen. Es macht mir nichts aus, mich irgendwann damit auseinanderzusetzen und zu sehen, ob ich einen Weg finden kann, die gewünschten Typinformationen beizubehalten, ohne die Tür für alles andere weit offen zu öffnen.

Dies ist ein weiterer Punkt von Redux: Sie erhalten Hot Reloading, Record/Replay kostenlos, ohne serialize(), hydrate() usw.

Das ist ein großer Vorteil von Redux und ich möchte das nicht ändern. Aber ich würde auch gerne einige Kompilierzeitgarantien für Aktionen bei der Verwendung von Redux erhalten.

Ich möchte es aus den oben genannten Gründen nicht im Kern ändern. Wie ich bereits sagte, können Sie gerne ein redux-typescript Paket mit einer TS-freundlichen Version von createStore und (vielleicht?) anderen Redux-Funktionen erstellen. Wir beabsichtigen zu diesem Zeitpunkt keine Breaking Changes in Redux, also sollten Sie sich keine Sorgen machen, mit der API Schritt zu halten. Und solange die API übereinstimmt, funktioniert ein solcher benutzerdefinierter Store einwandfrei. (Und seine Implementierung ist winzig, sieh dir die createStore Quelle an).

Ich denke, Sie können das Problem lösen, indem Sie benutzerdefinierte Typwächter von TypeScript 1.6 verwenden (siehe http://blogs.msdn.com/b/typescript/archive/2015/09/16/announcing-typescript-1-6.aspx) .

Definieren Sie eine Schnittstelle für die Basisaktion und eine für jede andere Aktion. Erstellen Sie zusätzlich für jede Aktion eine isXYZAction(a: Action) Funktion, die den Typ anhand des Werts des Typfelds (das in der Basisaktionsschnittstelle definiert ist) überprüft.

Verwenden Sie diese Funktionen im Reduzierer, um nach der Prüfung sicher auf Felder der spezifischen Aktionen zuzugreifen:

interface Action {
  type: string
}

interface AddTodoAction extends Action {
  text: string
}

function isAddTodoAction(action: Action): action is AddTodoAction {
  return action.type == ADD_TODO;
}

function reducer(state: any, action: Action) {
  if (isAddTodoAction(action)) {
    // typesafe access to action.text is possible here
  } else {
    return ...
  }
}

@Matthias247 genau das brauche ich, danke!

@Matthias247 Das ist fantastisch, danke für den Hinweis! Wusste nicht von dieser Funktion, genau das brauche ich :)

Ich verwende FSA und fand es sehr praktisch, eine generische Action Schnittstelle mit einem Typparameter zu haben, der dem Typ payload :

interface Action<T>{
  type: string;
  payload: T;
  error?: boolean;
  meta?: any;
}

Dann exportiere ich den Nutzlasttyp neben dem Aktionstyp mit demselben Namen:

export const ADD_TODO = 'ADD_TODO';
export type ADD_TODO = {text: string};

Auf diese Weise importieren Sie beim Importieren von ADD_TODO tatsächlich sowohl den Aktionstyp als auch den Nutzlasttyp:

import {ADD_TODO} from './actions';
import {handleActions} from 'redux-actions';

const reducer = handleActions({
  [ADD_TODO]: (state, action:Action<ADD_TODO>) => {
    // enabled type checking for action.payload
    // ...
  }
});

Ich hatte das gleiche Problem und habe die Lösung hier gefunden: http://www.bluewire-technologies.com/2015/redux-actions-for-typescript/. Es ist im Grunde eine Verallgemeinerung der von @Matthias247 vorgeschlagenen Lösung und hat einen klaren Vorteil: Es ist nicht erforderlich, für jede Aktion eine is*Action()-Funktion zu definieren.

Diese Lösung scheint zu funktionieren, benötigt jedoch 2 Ergänzungen für Redux. Zuerst benötigen Sie eine Middleware, um Aktionen in einfache Objekte umzuwandeln (redux führt diese Überprüfung durch und gibt einen Fehler aus). Dies könnte so einfach sein wie:

// middleware for transforming Typed Actions into plain actions
const typedToPlain = (store: any) => (next: any) => (action: any) =>  {
    next(Object.assign({}, action));
};

Und wenn Sie tslint verwenden, wird es sich über die nicht verwendete _brand-Eigenschaft beschweren, also können Sie es nur in tslint-Ignorier-Kommentare einschließen:

/* tslint:disable */
private _brand: NominalType;
/* tslint:enable */

Übrigens, ich habe NominalType nicht verwendet, da es der Boilerplate hinzuzufügen schien, und habe nur den void-Typ verwendet.

Für diejenigen, die an einer 'flux'-Bibliothek mit nativer Unterstützung für Typoskript interessiert sind, sollten Sie Folgendes überprüfen: https://github.com/AlexGalays/fluxx#typescript -store

@AlexGalays Bisher gute Arbeit! Haben Sie nicht daran gedacht, stattdessen Redux zu forken und TypeScript-Unterstützung hinzuzufügen, damit die Community alle Funktionen erhält, die Redux und seine Freundesmodule bieten, wie DevTools, Middleware, Sagas usw.?

UPD Oh, ich sehe, dieses https://github.com/rackt/redux/issues/992#issuecomment -153662848 beantwortet meine Frage. Warum dann fluxx und nicht nur redux-typescript ? Und ist es mit Redux-basierten Tools kompatibel?

UPD2 Wenn man genauer hinsieht, ist die einzige Ergänzung zu Redux die typings und tsconfig.json -- können wir nicht nur ein Repo mit diesen erhalten und die Wartung von Redux wiederverwenden und an Redux-Entwickler delegieren, um zu vermeiden, dass all dies callbacks und Dispatch in the middle of dispatch und Store.log in der TypeScript-orientierten Bibliothek implementiert wird?

@sompylasar Ich habe mir erwarten möchte"; Aber 1) ein bisschen Konkurrenz ist immer gut (allgemein gesprochen: fluxx wird offensichtlich durch Redux-Wettbewerbe ausgelöscht), 2) ich möchte es für meine Mitarbeiter einfach und leicht erlernbar halten. Das ist, was ich mit fluxx bekomme, was ich mit redux nicht bekomme: Nur eine npm-Abhängigkeit für alle meine Bedürfnisse (aber es ist noch winziger als Redux, kannst du überprüfen!), weniger Boilerplate (die Aktionen sind selbstabfertigend und sehr schnell zu erledigen .) neue schreiben) und die Garantie, dass meine Typoskriptdefinitionen auf dem neuesten Stand bleiben (ich glaube nicht an externe Definitionen, außer an Dinge wie Reagieren, bei denen Sie ziemlich sicher sein können, dass verschiedene Leute es auf dem neuesten Stand halten)

Auf der anderen Seite brauche ich viele Dinge nicht, die Redux und seine verschiedenen Erweiterungen bieten.

Übrigens, mein abyssa-Router bekommt demnächst auch Typoskript-Definitionen und ich habe die Arbeit mit der Kombination von abyssa/fluxx/immupdate als sehr angenehm empfunden.

weniger Boilerplate (die Aktionen werden selbst ausgeführt und es ist sehr schnell, neue zu schreiben)

FWIW, von http://redux.js.org/docs/recipes/ReducingBoilerplate.html :

Aktionen sind einfache Objekte, die beschreiben, was in der App passiert ist, und dienen als einzige Möglichkeit, die Absicht zu beschreiben, die Daten zu mutieren. Es ist wichtig, dass Aktionen, die Objekte sind, die Sie versenden müssen, kein Standardwerk sind, sondern eine der grundlegenden Designentscheidungen von Redux.

Es gibt Frameworks, die behaupten, Flux zu ähneln, aber ohne ein Konzept von Aktionsobjekten. In Bezug auf die Vorhersehbarkeit ist dies ein Rückschritt von Flux oder Redux. Wenn keine serialisierbaren einfachen Objektaktionen vorhanden sind, ist es unmöglich, Benutzersitzungen aufzuzeichnen und wiederzugeben oder Hot-Reloading mit Zeitreisen zu implementieren. Wenn Sie Daten lieber direkt ändern möchten, benötigen Sie Redux nicht.

Ein interessantes konkurrierendes Projekt, das TypeScript-freundlich ist, ist https://github.com/ngrx/store
Es ist nicht vollständig Redux-kompatibel, aber die Leute verwenden es gerne mit Angular.

@gaearon Ich weiß, warum du es getan hast, ich sage nicht "Redux könnte weniger Boilerplate haben". Ich habe gerade andere Kompromisse gewählt :p

https://github.com/ngrx/store verwendet Typoskript, ist aber überhaupt nicht typsicher. Sie können alles versenden, und Sie haben keinen Typhinweis in der/den Update-Funktion(en).

Ich habe gerade andere Kompromisse gewählt :p

:+1:

Sie können alles versenden, und Sie haben keinen Typhinweis in der/den Update-Funktion(en).

Danke, du scheinst recht zu haben. Möchten Sie dies mitbringen? Ich frage mich, ob das beabsichtigt ist.

Bitte helfen Sie mit, die neuen offiziellen Eingaben in https://github.com/reactjs/redux/pull/1413 zu überprüfen

Hier ist eine Kombination der obigen Ideen von type-guards und type name wie string const :

Der Vorteil besteht darin, dass Sie nur eine Typeguard-Funktion benötigen, die generisch ist und über alle Reducer hinweg verwendet werden kann. Ich bin mir jedoch nicht sicher, ob es funktioniert, habe es noch nicht in realen Projekten verwendet :-).

type Action<TPayload> = {
    type:string,
    payload:TPayload
}

const FOO_ACTION = 'FOO_ACTION';
type FOO_ACTION = { foo:string };
const BAR_ACTION = 'BAR_ACTION';
type BAR_ACTION = { bar:number };

// This is the only type-guard function needed since it is generic
function is<TPayload>(action: Action<any>, actionName:string): action is Action<TPayload> {
    return action.type == actionName;
}

function reducer(state:string, action:Action<any>) {
    if(is<FOO_ACTION>(action, FOO_ACTION)) {
        // Type-safe access to payload
        const x:string = action.payload.foo;
        //...
        return state;
    }
    else if(is<BAR_ACTION>(action, BAR_ACTION)) {
        // Type-safe access to payload
        const x:number = action.payload.bar;
        // ...
        return state;
    }
}

Mein neuster Ansatz ermöglicht es, Boilerplate für die Angabe von Typen entlang von Aktionstyp-Strings zu eliminieren:

interface ActionCreator<P> {
  type: string;
  (payload: P): Action<P>;
}

function actionCreator<P>(type: string): ActionCreator<P> {
  return Object.assign(
    (payload: P) => ({type, payload}),
    {type}
  );
}

export function isType<P>(action: Action<any>, 
                          actionCreator: ActionCreator<P>): action is Action<P> {
  return action.type === actionCreator.type;
}

Auf diese Weise kann ich eine Aktion mit einer einzigen Anweisung einrichten:

const fooAction = actionCreator<{foo: string }>('FOO_ACTION');

Und verwenden Sie es in Reduzierstücken:

const reducer = (state: State, action: Action<any>): State => {
  if (isType(action, fooAction)) {
    // action payload type is inferred to `{foo: string}`
  }

  return state;
}

Ich füge der Funktion actionCreator auch eine zusätzliche Prüfung hinzu, um sicherzustellen, dass Aktionstypen eindeutig sind.

@aikoven Das sieht nach einem interessanten Ansatz aus. Im Grunde ist die Action-Creator-Funktion also dafür verantwortlich, die String-Konstante herumzutragen. Wenn Sie redux-saga oder ähnliches verwenden möchten und die String-Konstante für take() benötigen, können Sie den action-creator foo importieren und dann foo.type verwenden, um die String-Konstante zu erhalten. Das einzige, was ich nicht verstehe, ist, wie Sie diesen Teil machen:

Ich füge der actionCreator-Funktion auch eine zusätzliche Prüfung hinzu, um sicherzustellen, dass Aktionstypen eindeutig sind.

@aikoven

Bearbeiten: String vs. String ist ein Problem. Nicht sicher, ob das behoben werden kann.

Das Importieren des Action Creators in den Reducer erschien mir etwas unintuitiv. Es ist jedoch schön, überhaupt keinen konstanten String definieren zu müssen.

Wie wäre es stattdessen mit der Eingabe der konstanten Zeichenfolge mit einem Generikum?

interface ActionType<TPayload> extends String {}

type Action<TPayload> = {
    type: ActionType<TPayload>,
    payload: TPayload
}

interface ActionCreator<P> {
    (payload: P): Action<P>;
}

function actionCreator<TPayload>(type: ActionType<TPayload>): ActionCreator<TPayload> {
    return (payload) => ({
        type,
        payload
    });
}

export function isType<TPayload>(
    action: Action<any>, 
    type: ActionType<TPayload>
): action is Action<TPayload> {
    return action.type === type;
}

Die type-Eigenschaft in der Aktion ist immer noch nur eine Zeichenfolge, aber sie ist so typisiert, dass sie ein Generikum des Typs ist, auf den ich achten möchte.

// In actions.ts
export const FOO: ActionType<{ foo: string }> = 'FOO';

export const createFoo = actionCreator(FOO);

Die Verwendung ist ganz klar:

// In reducer.ts
const reducer = (state: State, action: Action<any>): State => {
    if (isType(action, FOO)) {

        let foo:string = action.payload.foo;
    }

    return state;
}

@jonaskello

Wenn Sie redux-saga oder ähnliches verwenden möchten und die String-Konstante für take() benötigen, können Sie den action-creator foo importieren und dann foo.type verwenden, um die String-Konstante zu erhalten.

Exakt.

Das einzige, was ich nicht verstehe, ist, wie Sie diesen Teil machen:

Ich füge der actionCreator-Funktion auch eine zusätzliche Prüfung hinzu, um sicherzustellen, dass Aktionstypen eindeutig sind.

Einfach:

const actionTypes = {};

export function actionCreator<P>(type: string): ActionCreator<P> {
  if (actionTypes[type])
    throw new Error(`Duplicate action type: ${type}`);

  actionTypes[type] = true;

  return Object.assign(
  // ...

@geon Das ist schlau, obwohl es immer noch Boilerplate gibt, um den Aktionstyp zu definieren. Ich sehe keine Nachteile beim Importieren von Action Creators in Reducer. Tatsächlich sind alle meine Aktionsersteller einfache reine Funktionen und sie kapseln den Aktionstyp-String und den Aktions-Nutzlasttyp, mehr nicht. Und beide werden in Reduzierern verwendet, daher scheint es mir in Ordnung zu sein.

@aikoven Danke, das hast du wohl gemeint :-). Übrigens, wie gehen Sie mit Aktionen um, die keine Nutzlast haben? Nur den Nutzlasttyp auf any zu setzen, scheint keine schöne Lösung zu sein...

@geon @aikoven @jonaskello Ich bin mir nicht sicher, ob Ihre Lösungen das oben erwähnte nominelle

@igorbt Die type-guard-Funktion untersucht den action.type-String, um festzustellen, welchen Typ die Aktion hat. Die Form sollte also keine Rolle spielen.

@jonaskello du hast recht! Ich wurde durch den hier abgebildeten Anwendungsfall für nominales Tippen @geons Lösung angepasst, also ist das IMHO eine allgemeinere Lösung:

interface ActionType<TAction> extends String {}

interface Action {
    type: string;
}

interface FooAction extends Action {
    foo: string;
}

interface BarAction extends Action {
    bar: string;
}

function isType<T extends Action>(
    action: Action,
    type: ActionType<T>
): action is T
function isType<T extends Action>(
    action: Action,
    type: string
): action is T
{
    return action.type === type;
}

const FOO: ActionType<FooAction> = 'FOO';
const BAR: ActionType<BarAction> = 'BAR';
const BAZ = 'BAZ';

export const reducer = (state: any, action: Action): any => {
    if (isType(action, FOO)) {

        let foo = action.foo;
    }
    if (isType(action, BAR)) {

        let bar = action.bar;
        action.foo; // error
    }
    if (isType(action, BAZ)) {
        action.type; // BAZ
        action.foo; // error
    }
    return state;
}

@jonaskello

Übrigens, wie gehen Sie mit Aktionen um, die keine Nutzlast haben? Nur den Nutzlasttyp auf any zu setzen, scheint keine schöne Lösung zu sein...

Ich verwende den Parameter vom Typ void für sie, obwohl ich, bis dies behoben ist, das Argument payload des resultierenden Aktionserstellers optional machen muss:

interface ActionCreator<P> {
  type: string;
  (payload?: P): Action<P>;  // payload is optional
}

const fooAction = actionCreator<void>('FOO_ACTION');

const action = fooAction();

Dies ist jedoch offensichtlich nicht so typsicher. Eine andere Möglichkeit wäre, void 0 als Parameter zu übergeben, aber ich habe hier die Kürze der Sicherheit vorgezogen, da ich viele Aktionen ohne Nutzlast habe.

@igorbt , elegante Lösung. Beseitigt den nominellen Tippfehler, die nicht benötigten Klassen und das Risiko, versucht zu werden, instanceof zu verwenden.

Als Idee könnte es möglich sein, für die Konstante ActionType denselben Namen wie für die Aktionsschnittstelle zu verwenden, ohne dass sie in Konflikt geraten. Konstante und Schnittstelle werden wahrscheinlich an derselben Stelle definiert, wenn die App modular ist.

Ich überlege auch, ob es in Zukunft vielleicht irgendwie möglich ist, Strings durch Symbole zu ersetzen. So wie es aussieht, müssen wir für jeden Aktionstyp einige eindeutige Zeichenfolgen generieren, während wir immer noch etwas menschenlesbares haben.

@aikoven Danke, mache es jetzt genauso, da der Vorschlag zum Ignorieren der Leere genehmigt ist

@igorbt @use-strict Ich habe mit der von @geon vorgeschlagenen Lösung string . Z.B. dieser Code wird nicht funktionieren:

const FOO: ActionType<FooAction> = 'FOO';
const myString:string = FOO;

Das Problem liegt darin, dass es in JS einen Unterschied zwischen String und string ein TS kann nur String nicht string . Dies wird zu einem Problem, wenn Sie Ihren action.type-String an eine andere API wie redux-saga senden möchten. Da diese API ein string erwartet, Sie jedoch ein String senden. Natürlich können Sie in diesen Fällen eine Typumwandlung durchführen, aber am Ende habe ich mich für die von @aikoven vorgeschlagene Lösung eingebettet ist.

@use-strict Die Verwendung von Symbol für Aktionen wurde oft diskutiert und ist keine gute Idee, da Symbol nicht serialisierbar ist. Wenn Sie also Ihre Aktionen später woanders hinschicken möchten (lokaler Speicher, http etc.), funktioniert es nicht. Also sollten wir Strings verwenden. Die von @aikoven vorgeschlagene Lösung

Ein Teil der @aikoven- Lösung, den ich nur schwer verstehen konnte, war dieser:

interface ActionCreator<P> {
  type: string;
  (payload: P): Action<P>;
}

Zur Verdeutlichung: Es sieht so aus, als ob dieser Typ ein Objekt spezifiziert, aber tatsächlich spezifiziert er eine einzelne Funktion. Er benutzt den Trick, dass Funktionen in JS Objekte sind. Sie können also eine Eigenschaft an eine Funktion anhängen. Dieser Typ spezifiziert also eine Funktion vom Typ (payload: P) => Action<P> , sagt aber auch, dass diese Funktion eine angehängte Eigenschaft namens "type" haben kann. Ich finde diese TS-Syntax ziemlich funky, aber sie funktioniert.

Tatsächlich denke ich, dass die Idee, die Zeichenfolge an die Aktionserstellungsfunktion anzuhängen, außerhalb von TS Vorteile hat. Hier ist ein einfaches ES6-Beispiel, das möglicherweise einfacher zu verstehen ist (nicht getestet, aber ich denke, es funktioniert :-)). Beachten Sie, dass wir keine separate Zeichenfolgenkonstante mehr deklarieren müssen, da die Zeichenfolge in die Aktionserstellungsfunktion eingebettet ist:

// In actions.js
export function fooAction(foo:string) {
    return {
        foo: foo;
    }
}
fooAction.type = 'FOO';

// In reducer.js
import {fooAction} from './actions';

function(state, action) {
    if(action.type === fooAction.type) {
        // Do foo action stuff
    }
}

Vielleicht könnte dies als reduzierendes Boilerplate-Rezept in den Dokumenten hinzugefügt werden.

@aikoven @jonaskello Deine Lösung
@jonaskello Ich denke, in Ihrem ES6-Beispiel sollten Sie auch die actionCreator-Factory verwenden, sonst funktioniert es nicht.

Nachdem ich mit den tollen Vorschlägen hier (hauptsächlich @jonaskello 's) gespielt habe, habe ich zwei kleine Nachteile gefunden, die ich überwinden wollte:

  • Es ist etwas umständlich, Action Creators in den Reducern zu verwenden. Fühlt sich seltsam an mit dem Camel-Gehäuse, keine Schrifthervorhebungen und sie nur im Allgemeinen da zu haben.
  • Die Notwendigkeit eines Action Creators an erster Stelle. Ich habe eine hauptsächlich servergesteuerte Anwendung, bei der viele Aktionen auf dem Server instanziiert und die Typdefinitionen generiert werden.

Hier ist mein Vorschlag:

export class ConnectAction {
    static type = "Connect";
    host: string;
}

// Variant1: less boilerplate to create
export const connect1 = common.createAction(ConnectAction)

// Variatn2: less boilerplate to call
export const connect2 = common.createActionFunc(ConnectAction, (host: string) => ({ host: host}))

Aufrufen von Aktionserstellern:

connect1({ host: 'localhost'}) 
connect2('localhost');

Reduzierstück:

const reducer = (state: any = {}, action: Action) => {
    if (is(action, ConnectAction)) {
        console.log('We are currently connecting to: ' + action.host);
        return state;
    } else {
        return state;
    }
}

Das allgemeine/allgemeine Zeug:

export interface ActionClass<TAction extends Action> {
    type: string;
    new (): TAction;
}

export interface Action {
    type?: string;
}

export function createAction<TAction extends Action>(actionClass: ActionClass<TAction>): (payload: TAction) => TAction {
    return (arg: TAction) => {
        arg.type = actionClass.type;
        return arg;
    };
}

export function createActionFunc<TAction extends Action, TFactory extends (...args: any[]) => TAction>(
    actionClass: ActionClass<TAction>,
    actionFactory: TFactory): TFactory {
    return <any>((...args: any[]): any => {
        var action = actionFactory(args);
        action.type = actionClass.type;
        return action;
    });
}

export function is<P extends Action>(action: Action, actionClass: ActionClass<P>): action is P {
    return action.type === actionClass.type;
}

Die Nachteile dieser Lösung sind:

  • Unkonventionelle Verwendung von Klassen - sie werden nie instanziiert.
  • Müssen die type-Eigenschaft in der Actions-Schnittstelle optional machen (dies ist super untergeordnet).
  • Es ist umständlich, den Aktionen optionale Eigenschaften hinzuzufügen, da es sich um Klassen handelt (müssen eine zusätzliche Schnittstelle hinzugefügt werden).

Trotz der neuen Nachteile funktioniert diese Lösung für meine aktuellen Anwendungsfälle besser. Ich verwende auch nicht den Flux Standard Action Stil, aber ich wette, man kann mit meinem Vorschlag eine solche Variante machen.

Beachten Sie, dass wir in 3.5.x einige Eingabedateien hinzugefügt haben.
Wenn diese nicht so hilfreich sind, können Sie ein Problem ansprechen, um zu besprechen, wie sie verbessert werden können.

@Cooke siehe https://github.com/reactjs/redux/issues/992#issuecomment -191219795. Ich sehe nichts Falsches daran, Aktionsersteller in Reduzierern zu haben, da sie nur die Aktionstypzeichenfolge und die Nutzlastform kapseln. Natürlich gibt es auch eine Aktionsinstanziierungslogik, aber bei Klassen gibt es keinen Unterschied: Auch sie enthalten ihre eigenen Typinformationen zusammen mit der Konstruktionslogik.

@geon In Bezug auf das Problem "String vs. String" wie wäre es mit:

export interface _ActionType<T>{}
export type ActionType<T> = string & _ActionType<T>

// important to use _ActionType here
export function isAction<T extends Action>(action: Action, type: _ActionType<T>): action is T;
export function isAction<T extends Action>(action: Action, type: string): action is T {
    return action.type === type
}

Sie können dies wie zuvor verwenden, aber es ist eine normale Zeichenfolge und keine Zeichenfolge.

Dieser Ansatz funktioniert gut...

1) Erhalten Sie getippte Aktionen und Nutzlast zum Versenden.
2) Redux erhält saubere einfache Objekte
3) Reducers erhält typisierte Aktionstypen und typisierte Nutzlasten.

Erste Setup-Enumerationen..

export enum ActionTypes { TYPE_SELECTED }

create your commands...

export class ActionSelectedCommand  {
  constructor(public actionitem: any) {
  }
}

Erstellen Sie Ihre beim Dispatching aufzurufende Funktion (beachten Sie, dass ich ein Standardobjekt erstelle, das in gewisser Weise mein Befehlsobjekt serialisiert).

Jede Aktion hat also immer einen Typ und eine Nutzlast.

export function ActionSelected(actionItem): any {
  return {
    type: ActionTypes.TYPE_SELECTED,
    payload: JSON.stringify(new ActionSelectedCommand(actionItem))
  };
}

Dann auf deinem Reduzierstück...

  if (!state)
    state = new NewStore();

  let type: actions.ActionTypes = <actions.ActionTypes>action.type;  -- constant action type :)

  switch (type) {
    case actions.ActionTypes.TYPE_SELECTED: {  -- more constact action type
      var warmAndFuzzyPayLoad = <actions.ActionSelectedCommand>JSON.parse(action.payload)
      -- Then you deserialize back to your action command with all your parameters type set.
      return state;
    }
    default:
      return state;
  }

Dann abschicken...

store.dispatch(actions.ActionSelected("Tight Actions"));

Bisher scheint dies gut zu funktionieren.

Es zwingt dazu, die Aktionsbefehle sehr einfach zu halten, da jede Rekursion oder Selbstreferenz dies sprengen wird.

Ich mag die Idee, den Redux-Store-Typ frei zu halten. Dies ermöglicht die Flexibilität innerhalb und außerhalb des Busses.

Beifall

Ich denke, dass diskriminierte Unionstypen, die in TypeScript 2.0 eintreffen, eine gute Lösung für dieses Problem sein sollten.

Meine Traum-Action-Unterstützung wäre so etwas wie elm oder scala.js, wo der Reducer durch Pattern-Matching betrieben wird. Aber es scheint, dass Typoskript in naher Zukunft keinen Mustervergleich haben würde :(

Eine andere Version mit einfachen Objekten im Reducer, die hauptsächlich auf @Cooke basiert.
Es ermöglicht das Inline-Einfügen von Aktionsnutzlastfeldern und das Auslassen einer expliziten Aktionsdefinition.
ActionTypes werden hier als generische Aktionsersteller verwendet.

export interface Action {
    type?: string;
}
export class ActionType<T extends Action>{
    type: string;
    constructor(type: string) {
        this.type=type;
    }
    new(t: T):T {
        t.type=this.type;
        return t;
    }
}

export function isAction<T extends Action>(action: Action, type: ActionType<T>|string): action is T {
    if (typeof type === 'string') {
        return action.type==type;
    } else if (type instanceof ActionType) {
        return action.type === type.type;
    }
    return false;
}

// here come action creators
const FOO: ActionType<{foo: string, foo2: string}> = new ActionType<any>('FOO');
interface BarAction extends Action {
    bar: string;
}
const BAR: ActionType<BarAction> = new ActionType<BarAction>('BAR');

im Reduzierstück:

export default function reducer(state = [], action:Action) {
    if (isAction(action, FOO)) {
        action.foo // have access
        return state;
    } else if (isAction(action, BAR)) {
        action.bar;
        return state;
    } else
        return state;
}

Erstellen Sie eine neue Aktion wie diese

FOO.new({foo: 'foo'}) // error, foo2 is required
FOO.new({foo: 'foo', foo2: 'foo'}) // OK

// dispatch it like this
dispatch(FOO.new({foo: 'foo', foo2: 'foo'}));

Sie haben eine typsichere Nutzlast und müssen keinen bestimmten Aufgabenersteller verwenden.
Sie können immer noch Aktionen auf die alte Weise erstellen. Der Hauptnachteil ist, dass der Typ in der Aktionsschnittstelle nicht erforderlich ist.

Hier ist eine neue Version basierend auf den Vorschlägen von @aikoven , @jonaskello und inspiriert von redux-actions .
Das Ziel besteht darin, diese spezifischen Szenarien anzugehen:

  1. Typsicherheit für Zustand, Aktionen und Nutzlast
  2. Erlauben Sie die gemeinsame Nutzung von typsicheren Status- und lokalen Variablen für alle Handler innerhalb von Reduzierfunktionen
  3. Behalten Sie die Standard-Syntax für Action-Creator-Funktionen bei, um Logik, Thunks usw. zu ermöglichen.
  4. Schaltkasten-Boilerplate in Reduzierstücken reduzieren

Hier ist es, lassen Sie es mich wissen, wenn Sie größere Mängel sehen!

[BEARBEITEN: Die HandleActions-Funktion gemäß den Kommentaren von

// FSA Compliant action
interface Action<P> {
    type: string,
    payload?: P,
    error?: boolean,
    meta?: any
}

interface Todo {
    text: string
}

type HandlerMap<T> = {
    [type: string]: { (action: Action<any>): T }
}

// Handle actions "inspired" by redux-actions
// Only pass the action parameter to handlers, allowing to share type-safe state in the reducer
export const DEFAULT = "DEFAULT";
    export function handleAction<T>(
    action: Action<any>,
    handlers: HandlerMap<T>,
    defaultState: T): T {
    // As improved by <strong i="18">@alexfoxgill</strong>
    const handler = handlers[action.type] || handlers[DEFAULT];
    // Execute the function if exists passing the action and return its value.
    // Otherwise return the Default state. 
    return handler ? handler(action) : defaultState;
}

    // If no handlers matched the type, execute the DEFAULT handler 
    if (handlers[DEFAULT]) {
        return handlers[DEFAULT](action);
    }

    // If DEFAULT not declared, return the state from the parameter
    return defaultState;
}

export const ADD_TODO = "ADD_TODO";
// Declare the type for the payload
export type ADD_TODO = Action<string>
// Standard action creator pattern with type safe return value
export const addTodo = (text: string): ADD_TODO => {
    // Allows for some complex logic, async behavior, thunks, etc.

    return {
        type: ADD_TODO,
        payload: text,
        meta: {
            foo: "Bar"
        }
    }
}

export const DELETE_TODO = "DELETE_TODO";
export type DELETE_TODO = Action<number>
export const deleteTodo = (index: number): DELETE_TODO => {
    // Some complex logic, async behavior, thunks, etc.

    return {
        type: DELETE_TODO,
        payload: index,
    }
}

// Standard pattern for creating reducers
// Type safety for state and return value, shared for all handlers
export const todoReducer = function (
    state: Todo[] = [],
    action: Action<any>): Todo[] {
    // Local variables can be declared and shared by all handlers
    let foo = action.meta && action.meta.foo;

    // Type safety for return value of state
    return handleActions<Todo[]>(action, {
        // Less boilerplate by removing the 'return' and 'break' statements
        // Payload is a string 
        [ADD_TODO]: ({payload}: ADD_TODO) => [...state, { text: payload }],
        // Payload is a number
        [DELETE_TODO]: ({payload}: DELETE_TODO) =>
            [...state.slice(0, payload), ...state.slice(payload + 1)],
        // For more complex actions, expand function
        [DEFAULT]: ({payload}) => {
            // Can handle other operations that don't depend on type
            if (foo) {
                console.log(foo);
            };
            return state;
        }
    },
        // In case DEFAULT is not implemented, return the default state 
        state);
}

// Dispatch in the context of a connected component
addTodo("Foo");
deleteTodo(0);

@Agalla81 Ich mag dieses Design. Ich bin jedoch verwirrt über die Implementierung von handleActions - wozu dient die Schleife? Würde folgendes nicht funktionieren:

type HandlerMap<T> = {
    [type: string]: { (action: Action<any>): T }
}

export function handleAction<T>(action: Action<any>, handlers: HandlerMap<T>, defaultState: T): T {    
    const handler = handlers[action.type] || handlers[DEFAULT];
    return handler ? handler(action) : defaultState;
}

Sie können für dieses Muster auch eine Hilfsfunktion wie folgt definieren:

type HandlerMapWithState<T> = {
    [type: string]: { (action: Action<any>): (state: T) => T }
}

export function createReducer<T>(defaultState: T, handlers: HandlerMapWithState<T>) {
    return (state: T = defaultState, action: Action<any>): T => {
        const handler = handlers[action.type] || handlers[DEFAULT];
        return handler ? handler(action)(state) : state;
    };
}

Damit können Sie einen Reduzierer mit dieser Syntax erstellen:

const reducer = createReducer(0, {
    INCREMENT: (a: INCREMENT) => s => s + a.payload.incBy,
    DECREMENT: (a: DECREMENT) => s => s - a.payload.decBy
});

@alexfoxgill Sie haben

Ich mag den createReducer-Ansatz, aber er kann mit einem der Punkte kollidieren, die ich ansprechen wollte:
"2. Erlauben Sie die gemeinsame Nutzung von typsicherem Zustand und _lokalen Variablen für alle Handler innerhalb von Reduzierfunktionen_".

  • Der Typ Sicherer Zustand wird tatsächlich mit der "Doppel"-Pfeilfunktion "=> s =>" geteilt.
  • Aber wie können gemeinsam genutzte lokale Variablen bei der Verwendung von createReducer definiert werden?
    In einigen Szenarien finde ich es sinnvoll, diese zu definieren, wenn es eine gemeinsame Logik gibt, die von mehreren Aktionshandlern innerhalb des Reducers wiederverwendet werden kann.

@ Agalla81 Ich denke, das ist ein fairer Punkt, obwohl ich noch nie eine Zeit

@alexfoxgill Hier ist ein Beispiel aus der Dokumentation von Redux, das ein solches
Quelle: http://redux.js.org/docs/recipes/reducers/UpdatingNormalizedData.html

// reducers/posts.js
function addComment(state, action) {
    const {payload} = action;
    const {postId, commentId} = payload;

    // Look up the correct post, to simplify the rest of the code
    const post = state[postId];

    return {
        ...state,
        // Update our Post object with a new "comments" array
        [postId] : {
             ...post,
             comments : post.comments.concat(commentId)             
        }
    };
}

Ich habe gerade ein Paket veröffentlicht, das meinen obigen Ansatz implementiert:
https://github.com/aikoven/redux-typescript-actions

Ich bin über diesen Thread gestolpert, als ich versuchte, konkrete Klassen für Aktionen wie @nickknw zu verwenden, um die

Mir wurde klar, dass mit Typescript 2.0 die Vorahnung von @frankwallis wahr geworden ist, also dachte ich, ich würde mit einem Beispiel für alle anderen aktualisieren, die hier landen.

/* define the waxing actions and create a type alias which can be any of them */
interface WaxOn
{
    type: "WaxOn";
    id: number;
    waxBrand: string;
}

function createWaxOnAction(id: number, waxBrand: string): WaxOn
{
    return { type: "WaxOn", id, waxBrand };
}

interface WaxOff
{
    type: "WaxOff";
    id: number;
    spongeBrand: string;
}

function createWaxOffAction(id: number, spongeBrand: string): WaxOff
{
    return { type: "WaxOff", id, spongeBrand };
}

type AnyWaxingAction = WaxOn | WaxOff;

/* define the dodging actions and create a type alias which can be any of them */
interface Bob
{
    type: "Bob";
    id: number;
    bobDirection: number[];
}

function createBobAction(id: number, bobDirection: number[]): Bob
{
    return { type: "Bob", id, bobDirection };
}

interface Weave
{
    type: "Weave";
    id: number;
    weaveDirection: number[];
}

function createWeaveAction(id: number, weaveDirection: number[]): Weave
{
    return { type: "Weave", id, weaveDirection };
}

type AnyDodgeAction = Bob| Weave;

type AnyActionAtAll = AnyWaxingAction | AnyDodgeAction;
// action must implement one of the Action interfaces or tsc will yell at you
const reducer = (state: State = {}, action: AnyActionAtAll) =>
{
    switch(action.type)
    {
        case "WaxOn":
            // Will only let you access id and waxBrand
        case "WaxOff":
            // Will only let you access id and spongeBrand
        case "Bob":
            // Will only let you access id and bobDirection
        case "Weave":
            // Will only let you access id and weaveDirection
        default:
            // Make sure there aren't any actions not being handled here
            // that are included in the union
            const _exhaustiveCheck: never = action;
            return state;
    }
}

Wenn Sie versehentlich zwei Aktionen mit demselben Typ-String benennen, schreit typescript Sie nicht unbedingt an, aber im Switch-Fall für diesen String lässt typescript Sie nur auf die Member zugreifen, die _beide_ Interfaces gemeinsam haben.

@awesson Sie möchten vielleicht behaupten, dass die Aktion im Fall default in der switch-Anweisung vom Typ never . Auf diese Weise können Sie sicher sein, dass alle Aktionen ausgeführt werden und Sie nicht vergessen, dem Schalter weitere case s hinzuzufügen, wenn Sie neue Aktionen erstellen. Weitere Informationen finden Sie auf dieser Seite im Abschnitt "Umfassende Prüfungen".

Danke @jonaskello. Das ist ein guter Punkt.

Ich habe mein Beispiel aktualisiert, aber es ist wahrscheinlich besser, sich nur das von Ihnen verlinkte Beispiel anzusehen. (Ich weiß nicht, wie ich das verpasst habe.)

Ein Hinweis zur erschöpfenden Prüfung ist, dass, wenn Sie gerade erst anfangen, Aktionen für ein neues Modul zu schreiben, das dieser Organisation folgt, und Sie bisher nur 1 Aktion haben, der Typalias keine Union ist und die erschöpfende Prüfung daher immer fehlschlägt.

Ich habe mir verschiedene Lösungen für die vollständige Tippunterstützung (Zustand und Aktionen) angesehen und festgestellt, dass https://gist.github.com/japsu/d33f2b210f41de0e24ae47cf8db6a5df + @awessons Lösung eine wirklich gute Kombination ist

Nachdem ich mit den verschiedenen Alternativen herumgespielt hatte, entschied ich mich für den folgenden Ansatz https://github.com/Cooke/redux-ts-simple

Sie können Union-Typen verwenden, aber Sie können auch ein großes typisiertes Aktionsobjekt verwenden.

// Before

{
    type: "UserLoaded",
    payload: {
        user: { id: 4 }
    }
}

// After

{
    type: "UserLoaded",
    userLoaded: {
        user: { id: 4 }
    }
}

Im ersten Fall gibt es viele verschiedene Aktionsobjekte, sodass Sie einen Unionstyp benötigen.

interface UserLoaded {
    type: "UserLoaded",        // redundant
    payload: {
        user: User;
    };
}

type MyAction = UserLoaded | OtherAction

Im zweiten Fall gibt es nur einen Aktionstyp mit vielen Nutzlastschlüsseln.

interface UserLoaded {
    user: User;
}

interface MyAction {
    type: string;
    userLoaded?: UserLoaded;
    otherAction?: OtherAction;
}

Sie können reduzieren, indem Sie die type Zeichenfolge abgleichen.

function userById(state = {}, action: MyAction) {
    if (action.type == "UserLoaded") {
        // action.userLoaded.user
    }
}

Sie können auch das Vorhandensein des Nutzlastschlüssels überprüfen.

function userById(state = {}, action: MyAction) {
    if (action.userLoaded) {
        // action.userLoaded.user
    }
}

Wenn Sie Präsenz verwenden, verlassen Sie sich nicht mehr auf type für die tatsächliche Reduzierung, sodass Sie sich keine Sorgen machen müssen, wenn jemand denselben Namen in einem anderen Teil Ihres Projekts verwendet hat. Sie können auch Aktionsnutzlasten, die sich auf ein bestimmtes Widget oder eine bestimmte Domäne beziehen, innerhalb eines übergeordneten Aktionsobjekts verschachteln, das Kontext enthält.

enum Field {
    Title,
    Body,
}

interface TextChange {
    field: Field;
    newText: string;
}

interface Submit {

}

interface NewPostForm {
    pageId: string;
    textChange?: TextChange;
    submit?: Submit;
}

interface MyAction {
    type: string;
    newPostForm?: NewPostForm;
}

Wenn Sie die Vereinigung von Aktionen aus Untermodulen wirklich lieben, können Sie immer noch die Schnittstellenvererbung verwenden.

interface UserLoaded {
    user: User;
}

interface UserAction {
    userLoaded?: UserLoaded;
}

interface MyAction extends UserAction {
    type: string;
}

Also ja, es funktioniert gut mit der JSON-Serialisierung, vorhandenen Redux-Tools und es ist eine schöne Balance zwischen dem Einkapseln einiger Teile und gleichzeitigem Zulassen von Querschnittsaktionen.

Ich denke, jeder hat die meisten Lösungen bereits erwähnt. Meine zwei Cent zu diesem Thema möchte ich nur auf den @aikoven- Ansatz meiner Lib sehr nah an seinem.

Der einzige Unterschied meines Ansatzes im Vergleich zu @aikoven besteht darin, dass wir unsere Aktionen anhand ihrer ClassAction.is(..) / ClassAction.type anstelle von isType(..) in Reduzierern oder Epen/Nebeneffekten verfolgen.

Hier, wie ich es gemacht habe, habe ich versucht, alle Boilerplates so weit wie möglich zu entfernen:

feature-x.actions.ts

import { defineAction, defineSymbolAction } from 'redux-typed-actions';
import { ItemX } from './feature-x.types';

// For this action we don't have any payload
export const FeatureXLoadAction = defineAction('[Feature X] Load');

// Let's have a payload, this action will carry a payload with an array of ItemX type
export const FeatureXLoadSuccessAction = defineAction<ItemX[]>('[Feature X] Load Success');

// Let's have a symbol action
export const FeatureXDummySymbolAction = defineSymbolAction<ItemX[]>('[Feature X] Dummy Started');

feature-x.component.ts

import { ItemX } from './feature-x.types';
import { FeatureXLoadAction, FeatureXLoadSuccessAction } from '../feature-x.actions';
...
store.dispatch(FeatureXLoadAction.get());

// or in epics or side effects
const payload: ItemX[] = [{ title: 'item 1' }, { title: 'item 2' }];
store.dispatch(FeatureXLoadSuccessAction.get(payload));
// And a more strict check for payload param
store.dispatch(FeatureXLoadSuccessAction.strictGet(payload));

feature-x.reducers.ts

import { PlainAction } from 'redux-typed-actions';
import { ItemX } from './feature-x.types';
import { FeatureXLoadAction, FeatureXLoadSuccessAction } from '../feature-x.actions';

export interface ItemXState {
  items: ItemX[];
  loading: boolean;
}

...
export function reducer(state: ItemXState = InitialState, action: PlainAction): ItemXState {
  if (FeatureXLoadAction.is(action)) { // Visually helps developer to keep track of actions
    // Within this branch our action variable has the right typings
    return {
      ...state,
      loading: true,
    }

  } else if (FeatureXLoadSuccessAction.is(action)) {
    return {
      ...state,
      loading: false,
      items: action.payload, // <- Here we are checking types strongly :)
    }

  } else {
    return state;
  }
}

feature-x.epics.ts (redux-observable-bezogen)

...
class epics {
   loadEpic(): Epic<PlainAction, AppState> {
    return (action$, _store) => action$
      .ofType(FeatureXLoadAction.type)
      .switchMap(() =>
        this.service.getRepositories()
          .map(repos => StoreService.transformToItem(repos))
          .delay(2000)
          .map(repos => FeatureXLoadSuccessAction.strictGet(repos))
          .catch((error) => Observable.of(FeatureXLoadFailedAction.strictGet(`Oops something went wrong:\n\r${error._body}`))));
}

Hier ein Live-Beispiel

@jonaskello ich mochte deinen Kommentar zu Symbolen. Du erwähnst:
If you want, you could modify the actionCreator() function in his solution to generate unique strings automatically since you will never refer to the strings directly in the code.
wenn ich mich nicht irre werden die devtools diese Strings anzeigen? Wir müssen immer noch eine eindeutige Zeichenfolge generieren, die in Bezug auf die tatsächliche Aktion und nicht nur auf eine UID sinnvoll ist.

Also ich denke, was @aikoven vorschlägt, um dieses Problem nicht zu haben, ist nicht so schlimm.

export function actionCreator<P>(type: string): ActionCreator<P> {
 if (actionTypes[type])
    throw new Error('Duplicate action type: ${type}');
...

@nasreddineskandrani Oder eine bessere Lösung wäre, process.env.NODE_ENV zu überprüfen und wenn es development warnen Sie den Entwickler und wenn es in Produktion ist, generieren Sie einfach einen zufälligen Hash und hängen Sie ihn an das Ende des Aktionstypwerts, um zu machen es einzigartig. Auf diese Weise wird es in einer großen Codebasis nie fehlschlagen.

@aminpaks Ich denke, devtool mit echtem Aktionsnamen in Prod ist nützlich.
https://medium.com/@zalmoxis/using -redux-devtools-in-production-4c5b56c5600f
Beispiel: What if we want the end-users to help the debugging

@nasreddineskandrani Ich habe nie gesagt, dass wir den ursprünglichen Aktionstyp entfernen sollten. Ich meinte, fügen wir am Ende einen Hash hinzu, damit wir eine einzigartige Aktion haben.

Einer der Entwickler in diesem Thread erwähnt dies:
.... I have a mostly server driven application where many actions are instantiated on the server and the type definitions are generated.
Berücksichtigen Sie bei einer dynamischen Typstrategie, dass Ihre Lib für eine bestimmte Aktion genau den gleichen Hash auf zwei verschiedenen Servern benötigt und nicht für eine zufällige.

Ich habe vor kurzem ein React-Beispiel mit redux-typed-actions hinzugefügt, um zu zeigen, wie diese Strategie zur Verbesserung der Produktivität beitragen kann und alle Boilerplates entfernt.

Schau mal hier .

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen