Quel est le meilleur moyen de transtyper le paramètre action
dans un redux reducer with typescript? Il peut y avoir plusieurs interfaces d'action pouvant toutes étendre une interface de base avec un type de propriété. Les interfaces d'action étendues peuvent avoir plusieurs propriétés qui sont toutes différentes entre les interfaces d'action. Voici un exemple ci-dessous:
interface IAction {
type: string
}
interface IActionA extends IAction {
a: string
}
interface IActionB extends IAction {
b: string
}
const reducer = (action: IAction) {
switch (action.type) {
case 'a':
return console.info('action a: ', action.a) // property 'a' does not exists on type IAction
case 'b':
return console.info('action b: ', action.b) // property 'b' does not exists on type IAction
}
}
Le problème est que action
doit être converti en un type ayant accès à IActionA
et IActionB
pour que le réducteur puisse utiliser à la fois action.a
et action.a
sans générer d'erreur.
J'ai plusieurs idées sur la façon de contourner ce problème:
action
à any
.exemple:
interface IAction {
type: string
a?: string
b?: string
}
Quelle est la meilleure façon d'organiser Action/Reducers en dactylographie? Merci d'avance!
Avec les types d'union marqués de TypeScript 2
interface ActionA {
type: 'a';
a: string
}
interface ActionB {
type: 'b';
b: string
}
type Action = ActionA | ActionB;
function reducer(action:Action) {
switch (action.type) {
case 'a':
return console.info('action a: ', action.a)
case 'b':
return console.info('action b: ', action.b)
}
}
J'ai une interface Action
export interface Action<T, P> {
readonly type: T;
readonly payload?: P;
}
J'ai une fonction createAction
:
export function createAction<T extends string, P>(type: T, payload: P): Action<T, P> {
return { type, payload };
}
J'ai une constante de type d'action:
const IncreaseBusyCountActionType = "IncreaseBusyCount";
Et j’ai une interface pour l’action (voyez l’utilisation géniale de typeof
):
type IncreaseBusyCountAction = Action<typeof IncreaseBusyCountActionType, void>;
J'ai une fonction créateur d'action:
function createIncreaseBusyCountAction(): IncreaseBusyCountAction {
return createAction(IncreaseBusyCountActionType, null);
}
Maintenant, mon réducteur ressemble à ceci:
type Actions = IncreaseBusyCountAction | DecreaseBusyCountAction;
function busyCount(state: number = 0, action: Actions) {
switch (action.type) {
case IncreaseBusyCountActionType: return reduceIncreaseBusyCountAction(state, action);
case DecreaseBusyCountActionType: return reduceDecreaseBusyCountAction(state, action);
default: return state;
}
}
Et j'ai une fonction réducteur par action:
function reduceIncreaseBusyCountAction(state: number, action: IncreaseBusyCountAction): number {
return state + 1;
}
Voici une solution intelligente de l'utilisateur Github aikoven from https://github.com/reactjs/redux/issues/992#issuecomment-191152574 :
type Action<TPayload> = {
type: string;
payload: TPayload;
}
interface IActionCreator<P> {
type: string;
(payload: P): Action<P>;
}
function actionCreator<P>(type: string): IActionCreator<P> {
return Object.assign(
(payload: P) => ({type, payload}),
{type}
);
}
function isType<P>(action: Action<any>,
actionCreator: IActionCreator<P>): action is Action<P> {
return action.type === actionCreator.type;
}
Utilisez actionCreator<P>
pour définir vos actions et créateurs d’actions:
export const helloWorldAction = actionCreator<{foo: string}>('HELLO_WORLD');
export const otherAction = actionCreator<{a: number, b: string}>('OTHER_ACTION');
Utilisez le type de garde défini par l'utilisateur isType<P>
dans le réducteur:
function helloReducer(state: string[] = ['hello'], action: Action<any>): string[] {
if (isType(action, helloWorldAction)) { // type guard
return [...state, action.payload.foo], // action.payload is now {foo: string}
}
else if(isType(action, otherAction)) {
...
Et pour envoyer une action:
dispatch(helloWorldAction({foo: 'world'})
dispatch(otherAction({a: 42, b: 'moon'}))
Je vous recommande de lire l'intégralité du fil de commentaire pour trouver d'autres options car plusieurs bonnes solutions sont également présentées.
Pour un réducteur relativement simple, vous pourriez probablement simplement utiliser des gardes de caractères:
function isA(action: IAction): action is IActionA {
return action.type === 'a';
}
function isB(action: IAction): action is IActionB {
return action.type === 'b';
}
function reducer(action: IAction) {
if (isA(action)) {
console.info('action a: ', action.a);
} else if (isB(action)) {
console.info('action b: ', action.b);
}
}
Voici comment je le fais:
IAction.ts
import {Action} from 'redux';
/**
* https://github.com/acdlite/flux-standard-action
*/
export default interface IAction<T> extends Action<string> {
type: string;
payload?: T;
error?: boolean;
meta?: any;
}
UserAction.ts
import IAction from '../IAction';
import UserModel from './models/UserModel';
export type UserActionUnion = void | UserModel;
export default class UserAction {
public static readonly LOAD_USER: string = 'UserAction.LOAD_USER';
public static readonly LOAD_USER_SUCCESS: string = 'UserAction.LOAD_USER_SUCCESS';
public static loadUser(): IAction<void> {
return {
type: UserAction.LOAD_USER,
};
}
public static loadUserSuccess(model: UserModel): IAction<UserModel> {
return {
payload: model,
type: UserAction.LOAD_USER_SUCCESS,
};
}
}
UserReducer.ts
import UserAction, {UserActionUnion} from './UserAction';
import IUserReducerState from './IUserReducerState';
import IAction from '../IAction';
import UserModel from './models/UserModel';
export default class UserReducer {
private static readonly _initialState: IUserReducerState = {
currentUser: null,
isLoadingUser: false,
};
public static reducer(state: IUserReducerState = UserReducer._initialState, action: IAction<UserActionUnion>): IUserReducerState {
switch (action.type) {
case UserAction.LOAD_USER:
return {
...state,
isLoadingUser: true,
};
case UserAction.LOAD_USER_SUCCESS:
return {
...state,
isLoadingUser: false,
currentUser: action.payload as UserModel,
};
default:
return state;
}
}
}
IUserReducerState.ts
import UserModel from './models/UserModel';
export default interface IUserReducerState {
readonly currentUser: UserModel;
readonly isLoadingUser: boolean;
}
UserSaga.ts
import IAction from '../IAction';
import UserService from './UserService';
import UserAction from './UserAction';
import {put} from 'redux-saga/effects';
import UserModel from './models/UserModel';
export default class UserSaga {
public static* loadUser(action: IAction<void> = null) {
const userModel: UserModel = yield UserService.loadUser();
yield put(UserAction.loadUserSuccess(userModel));
}
}
UserService.ts
import HttpUtility from '../../utilities/HttpUtility';
import {AxiosResponse} from 'axios';
import UserModel from './models/UserModel';
import RandomUserResponseModel from './models/RandomUserResponseModel';
import environment from 'environment';
export default class UserService {
private static _http: HttpUtility = new HttpUtility();
public static async loadUser(): Promise<UserModel> {
const endpoint: string = `${environment.endpointUrl.randomuser}?inc=picture,name,email,phone,id,dob`;
const response: AxiosResponse = await UserService._http.get(endpoint);
const randomUser = new RandomUserResponseModel(response.data);
return randomUser.results[0];
}
}
https://github.com/codeBelt/TypeScript-hapi-react-hot-loader-example
Plusieurs commentaires ci-dessus ont mentionné le concept/fonction `actionCreator´ - Jetez un oeil sur redux-actions package (Et les définitions correspondantes de TypeScript ), problème: création de fonctions de créateur d'action contenant des informations de type TypeScript spécifiant le type de charge utile de l'action.
La deuxième partie du problème consiste à combiner les fonctions de réducteur en un seul réducteur sans code passe-partout et de manière sûre pour le type (Comme la question a été posée sur TypeScript).
Combinez redux-actions Et redux-actions-ts-reducer packages:
1) Créez des fonctions actionCreator pouvant être utilisées pour créer des actions avec le type et les données souhaités lors de la distribution de l’action:
import { createAction } from 'redux-actions';
const negate = createAction('NEGATE'); // action without payload
const add = createAction<number>('ADD'); // action with payload type `number`
2) Créer un réducteur avec l’état initial et les fonctions de réducteur pour toutes les actions connexes:
import { ReducerFactory } from 'redux-actions-ts-reducer';
// type of the state - not strictly needed, you could inline it as object for initial state
class SampleState {
count = 0;
}
// creating reducer that combines several reducer functions
const reducer = new ReducerFactory(new SampleState())
// `state` argument and return type is inferred based on `new ReducerFactory(initialState)`.
// Type of `action.payload` is inferred based on first argument (action creator)
.addReducer(add, (state, action) => {
return {
...state,
count: state.count + action.payload,
};
})
// no point to add `action` argument to reducer in this case, as `action.payload` type would be `void` (and effectively useless)
.addReducer(negate, (state) => {
return {
...state,
count: state.count * -1,
};
})
// chain as many reducer functions as you like with arbitrary payload types
...
// Finally call this method, to create a reducer:
.toReducer();
Comme vous pouvez le constater à partir des commentaires, il n'est pas nécessaire d'écrire des annotations de type TypeScript, mais tous les types sont déduits (Cela fonctionne même avec noImplicitAny
option du compilateur TypeScript )
Si vous utilisez des actions provenant d'une structure qui n'expose pas les créateurs d'actions redux-action
(et que vous ne voulez pas non plus les créer vous-même) Ou que du code hérité utilisant des constantes de chaîne pour les types d'action, vous pouvez également ajouter des réducteurs :
const SOME_LIB_NO_ARGS_ACTION_TYPE = '@@some-lib/NO_ARGS_ACTION_TYPE';
const SOME_LIB_STRING_ACTION_TYPE = '@@some-lib/STRING_ACTION_TYPE';
const reducer = new ReducerFactory(new SampleState())
...
// when adding reducer for action using string actionType
// You should tell what is the action payload type using generic argument (if You plan to use `action.payload`)
.addReducer<string>(SOME_LIB_STRING_ACTION_TYPE, (state, action) => {
return {
...state,
message: action.payload,
};
})
// action.payload type is `void` by default when adding reducer function using `addReducer(actionType: string, reducerFunction)`
.addReducer(SOME_LIB_NO_ARGS_ACTION_TYPE, (state) => {
return new SampleState();
})
...
.toReducer();
il est donc facile de commencer sans refactoriser votre base de code.
Vous pouvez envoyer des actions même sans redux
comme ceci:
const newState = reducer(previousState, add(5));
mais l'envoi d'une action avec redux
est plus simple - utilisez la fonction dispatch(...)
comme d'habitude:
dispatch(add(5));
dispatch(negate());
dispatch({ // dispatching action without actionCreator
type: SOME_LIB_STRING_ACTION_TYPE,
payload: newMessage,
});
Confession: Je suis l'auteur de redux-actions-ts-reducer que j'ai ouvert aujourd'hui.
tu pourrais faire les choses suivantes
si vous attendez l'un des IActionA
ou IActionB
uniquement, vous pouvez limiter le type au moins et définir votre fonction comme
const reducer = (action: (IActionA | IActionB)) => {
...
}
Le problème, c’est que vous devez encore savoir de quel type il s’agit. Vous pouvez totalement ajouter une propriété type
, mais vous devez ensuite la définir quelque part et les interfaces ne sont que des superpositions sur des structures d'objet. Vous pouvez créer des classes d'action et faire en sorte que le ctor définisse le type.
Sinon, vous devez vérifier l'objet avec quelque chose d'autre . Dans votre cas, vous pouvez utiliser hasOwnProperty
et, en fonction de cela, le convertir dans le type correct:
const reducer = (action: (IActionA | IActionB)) => {
if(action.hasOwnProperty("a")){
return (<IActionA>action).a;
}
return (<IActionB>action).b;
}
Cela fonctionnerait encore une fois compilé en JavaScript.
Pour obtenir une sécurité de type implicite sans avoir à écrire des interfaces pour chaque action, vous pouvez utiliser cette approche (inspirée de la fonction returntypeof d’ici: https://github.com/piotrwitek/react-redux-TypeScript#returntypeof-polyfill )
import { values } from 'underscore'
/**
* action creator (declaring the return type is optional,
* but you can make the props readonly)
*/
export const createAction = <T extends string, P extends {}>(type: T, payload: P) => {
return {
type,
payload
} as {
readonly type: T,
readonly payload: P
}
}
/**
* Action types
*/
const ACTION_A = "ACTION_A"
const ACTION_B = "ACTION_B"
/**
* actions
*/
const actions = {
actionA: (count: number) => createAction(ACTION_A, { count }),
actionB: (name: string) => createAction(ACTION_B, { name })
}
/**
* create action type which you can use with a typeguard in the reducer
* the actionlist variable is only needed for generation of TAction
*/
const actionList = values(actions).map(returnTypeOf)
type TAction = typeof actionList[number]
/**
* Reducer
*/
export const reducer = (state: any, action: TAction) => {
if ( action.type === ACTION_A ) {
console.log(action.payload.count)
}
if ( action.type === ACTION_B ) {
console.log(action.payload.name)
console.log(action.payload.count) // compile error, because count does not exist on ACTION_B
}
console.log(action.payload.name) // compile error because name does not exist on every action
}
Avec TypeScript v2, vous pouvez le faire assez facilement en utilisant des types union avec des types gardes et les propres types Action et Reducer de Redux sans utiliser d'autres bibliothèques tierces appliquer une forme commune à toutes les actions (par exemple, via payload
).
De cette façon, vos actions sont correctement saisies dans vos clauses de blocage réducteur, de même que l'état renvoyé.
import {
Action,
Reducer,
} from 'redux';
interface IState {
tinker: string
toy: string
}
type IAction = ISetTinker
| ISetToy;
const SET_TINKER = 'SET_TINKER';
const SET_TOY = 'SET_TOY';
interface ISetTinker extends Action<typeof SET_TINKER> {
tinkerValue: string
}
const setTinker = (tinkerValue: string): ISetTinker => ({
type: SET_TINKER, tinkerValue,
});
interface ISetToy extends Action<typeof SET_TOY> {
toyValue: string
}
const setToy = (toyValue: string): ISetToy => ({
type: SET_TOY, toyValue,
});
const reducer: Reducer<IState, IAction> = (
state = { tinker: 'abc', toy: 'xyz' },
action
) => {
// action is IAction
if (action.type === SET_TINKER) {
// action is ISetTinker
// return { ...state, tinker: action.wrong } // doesn't typecheck
// return { ...state, tinker: false } // doesn't typecheck
return {
...state,
tinker: action.tinkerValue,
};
} else if (action.type === SET_TOY) {
return {
...state,
toy: action.toyValue
};
}
return state;
}
Things est en gros ce que @Sven Efftinge suggère, tout en vérifiant en outre le type de retour du réducteur.
La solution référencée @Jussi_K est Nice car elle est générique.
Cependant, j'ai trouvé un moyen que j'aime mieux, sur cinq points:
action.Is(Type)
, au lieu de la plus fonctionnelle isType(action, createType)
.type Action<TPayload>
, interface IActionCreator<P>
, function actionCreator<P>()
, function isType<P>()
.class MyAction extends Action<{myProp}> {}
.type
en calculant simplement type
comme étant le nom de la classe/du constructeur. Ceci est conforme au principe DRY, contrairement à l’autre solution qui a à la fois une fonction helloWorldAction
et une HELLO_WORLD
"chaîne magique".Quoi qu'il en soit, pour implémenter cette configuration alternative:
Commencez par copier cette classe d'action générique:
class Action<Payload> {
constructor(payload: Payload) {
this.type = this.constructor.name;
//this.payload = payload;
Object.assign(this, payload);
}
type: string;
payload: Payload; // stub; needed for Is() method's type-inference to work, for some reason
Is<Payload2>(actionType: new(..._)=>Action<Payload2>): this is Payload2 {
return this.type == actionType.name;
//return this instanceof actionType; // alternative
}
}
Ensuite, créez vos classes d'action dérivées:
class IncreaseNumberAction extends Action<{amount: number}> {}
class DecreaseNumberAction extends Action<{amount: number}> {}
Ensuite, à utiliser dans une fonction de réduction:
function reducer(state, action: Action<any>) {
if (action.Is(IncreaseNumberAction))
return {...state, number: state.number + action.amount};
if (action.Is(DecreaseNumberAction))
return {...state, number: state.number - action.amount};
return state;
}
Lorsque vous voulez créer et envoyer une action, faites simplement:
dispatch(new IncreaseNumberAction({amount: 10}));
Comme avec la solution de @ Jussi_K, chacune de ces étapes est sécurisée au type.
Si vous souhaitez que le système soit compatible avec les objets d'action anonymes (par exemple, à partir de code hérité ou d'état désérialisé), vous pouvez utiliser cette fonction statique dans vos réducteurs:
function IsType<Payload>(action, actionType: new(..._)=>Action<Props>): action is Payload {
return action.type == actionType.name;
}
Et utilisez-le comme suit:
function reducer(state, action: Action<any>) {
if (IsType(action, IncreaseNumberAction))
return {...state, number: state.number + action.amount};
if (IsType(action, DecreaseNumberAction))
return {...state, number: state.number - action.amount};
return state;
}
L'autre option consiste à ajouter la méthode Action.Is()
au Object.prototype
global à l'aide de Object.defineProperty
. C’est ce que je suis en train de faire - bien que la plupart des gens n’aiment pas cela, car cela pollue le prototype.
Malgré le fait que cela fonctionnerait de toute façon, Redux se plaint que "Les actions doivent être des objets simples. Utilisez un middleware personnalisé pour les actions asynchrones".
Pour résoudre ce problème, vous pouvez soit:
isPlainObject()
dans Redux.Action
: (elle supprime le lien d'exécution entre l'instance et la classe)Object.setPrototypeOf(this, Object.getPrototypeOf({}));
Je suis l'auteur de ts-redux-actions-réducteur-fabrique et vous présente cela comme une solution supplémentaire par rapport aux autres. Ce package déduit l'action par créateur d'action ou par type d'action défini manuellement et - c'est nouveau - l'état. Ainsi, chaque réducteur prend connaissance du type de retour des réducteurs précédents et représente donc un état étendu possible qui doit être initialisé à la fin, sauf si cela est fait au début. C'est un peu spécial dans son utilisation, mais cela peut simplifier la frappe.
Mais voici une solution complète possible sur la base de votre problème:
import { createAction } from "redux-actions";
import { StateType } from "typesafe-actions";
import { ReducerFactory } from "../../src";
// Type constants
const aType = "a";
const bType = "b";
// Container a
interface IActionA {
a: string;
}
// Container b
interface IActionB {
b: string;
}
// You define the action creators:
// - you want to be able to reduce "a"
const createAAction = createAction<IActionA, string>(aType, (a) => ({ a }));
// - you also want to be able to reduce "b"
const createBAction = createAction<IActionB, string>(aType, (b) => ({ b }));
/*
* Now comes a neat reducer factory into the game and we
* keep a reference to the factory for example purposes
*/
const factory = ReducerFactory
.create()
/*
* We need to take care about other following reducers, so we normally want to include the state
* by adding "...state", otherwise only property "a" would survive after reducing "a".
*/
.addReducer(createAAction, (state, action) => ({
...state,
...action.payload!,
}))
/*
* By implementation you are forced to initialize "a", because we
* now know about the property "a" by previous defined reducer.
*/
.addReducer(createBAction, (state, action) => ({
...state,
...action.payload!,
}))
/**
* Now we have to call `acceptUnknownState` and are forced to initialize the reducer state.
*/
.acceptUnknownState({
a: "I am A by default!",
b: "I am B by default!",
});
// At the very end, we want the reducer.
const reducer = factory.toReducer();
const initialState = factory.initialKnownState;
// { a: "I am A by default!", b: "I am B by default!" }
const resultFromA = reducer(initialState, createAAction("I am A!"));
// { a: "I am A!", b: "I am B by default!" }
const resultFromB = reducer(resultFromA, createBAction("I am B!"));
// { a: "I am A!", b: "I am B!" }
// And when you need the new derived type, you can get it with a module like @typesafe-actions
type DerivedType = StateType<typeof reducer>;
// Everything is type-safe. :)
const derivedState: DerivedType = initialState;
Certaines bibliothèques regroupent la plupart du code mentionné dans d'autres réponses: aikoven/TypeScript-fsa et dphilipson/TypeScript-fsa-reducers .
Avec ces bibliothèques, le code de vos actions et de vos réducteurs est statiquement typé et lisible:
import actionCreatorFactory from "TypeScript-fsa";
const actionCreator = actionCreatorFactory();
interface State {
name: string;
balance: number;
isFrozen: boolean;
}
const INITIAL_STATE: State = {
name: "Untitled",
balance: 0,
isFrozen: false,
};
const setName = actionCreator<string>("SET_NAME");
const addBalance = actionCreator<number>("ADD_BALANCE");
const setIsFrozen = actionCreator<boolean>("SET_IS_FROZEN");
...
import { reducerWithInitialState } from "TypeScript-fsa-reducers";
const reducer = reducerWithInitialState(INITIAL_STATE)
.case(setName, (state, name) => ({ ...state, name }))
.case(addBalance, (state, amount) => ({
...state,
balance: state.balance + amount,
}))
.case(setIsFrozen, (state, isFrozen) => ({ ...state, isFrozen }));
vous pouvez définir votre action comme suit:
// src/actions/index.tsx
import * as constants from '../constants'
export interface IncrementEnthusiasm {
type: constants.INCREMENT_ENTHUSIASM;
}
export interface DecrementEnthusiasm {
type: constants.DECREMENT_ENTHUSIASM;
}
export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;
export function incrementEnthusiasm(): IncrementEnthusiasm {
return {
type: constants.INCREMENT_ENTHUSIASM
}
}
export function decrementEnthusiasm(): DecrementEnthusiasm {
return {
type: constants.DECREMENT_ENTHUSIASM
}
}
et ainsi, vous pouvez définir votre réducteur comme suit:
// src/réducteurs/index.tsx
import { EnthusiasmAction } from '../actions';
import { StoreState } from '../types/index';
import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index';
export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
case DECREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1) };
}
return state;
}
Documents officiels complets: https://github.com/Microsoft/TypeScript-React-Starter#adding-a-reducer
Dernièrement, j'utilise cette approche:
export abstract class PlainAction {
public abstract readonly type: any;
constructor() {
return Object.assign({}, this);
}
}
export abstract class ActionWithPayload<P extends object = any> extends PlainAction {
constructor(public readonly payload: P) {
super();
}
}
export class BeginBusyAction extends PlainAction {
public readonly type = "BeginBusy";
}
export interface SendChannelMessageActionPayload {
message: string;
}
export class SendChannelMessageAction
extends ActionWithPayload<SendChannelMessageActionPayload>
{
public readonly type = "SendChannelMessage";
constructor(
message: string,
) {
super({
message,
});
}
}
Ça ici:
constructor() {
return Object.assign({}, this);
}
assure que les Action
s sont tous des objets simples. Maintenant, vous pouvez faire des actions comme ceci: const action = new BeginBusyAction()
. (oui\o /)
Si vous devez corriger votre implémentation exactement comme vous l'avez publiée, voici comment le corriger et le faire fonctionner à l'aide d'assertions de type, respectivement, comme le montre l'exemple suivant:
interface IAction {
type: string
}
interface IActionA extends IAction {
a: string
}
interface IActionB extends IAction {
b: string
}
const reducer = (action: IAction) => {
switch (action.type) {
case 'a':
return console.info('action a: ', (<IActionA>action).a) // property 'a' exists because you're using type assertion <IActionA>
case 'b':
return console.info('action b: ', (<IActionB>action).b) // property 'b' exists because you're using type assertion <IActionB>
}
}
Vous pouvez en apprendre plus sur la section "Types et types de différenciation" De la documentation officielle: https://www.typescriptlang.org/docs/handbook/advanced-types.html
Pour être juste, il y a beaucoup de façons de taper des actions mais je trouve celui-ci très simple et a aussi le passe-partout le moins possible (déjà traité dans ce sujet).
Cette approche tente de taper la clé appelée "charge utile" des actions.
_ { Voir cet exemple } _