the-dr-lazy / deox

Functional Type-safe Flux Standard Utilities
https://deox.js.org
MIT License
206 stars 12 forks source link

Create a "actionCreatorFactory" function #106

Closed LouizFC closed 2 years ago

LouizFC commented 5 years ago

Very often I need to make reusable redux components, and also often these components state appear more than once in the Redux store.

To solve this I usually follow this pattern:

export function createParamActionCreators(paramTableName: string) {
  return {
    setParams: createActionCreator(
      `[${paramTableName}] SET_PARAMS`,
      (resolve) => (param: Param[]) => {
        return resolve(param);
      }
    ),

    addParam: createActionCreator(
      `[${paramTableName}] ADD_PARAM`,
      (resolve) => (param: Param) => {
        return resolve(param);
      }
    ),
    ...
  };
}

type ParamTableActionCreators = ReturnType<typeof createParamActionCreators>;

export function createParamTableReducer(actions: ParamTableActionCreators) {
  return createReducer([] as Param[], (handle) => [
    handle(actions.setParams, (_state, { payload }) => payload),
    handle(actions.addParam, (state, { payload }) =>
      produce(state, (draft: Draft<Param[]>) => {
        draft.push(payload);
      })
    ),
    ...
  ]);
}

I am trying to implement it myself, you can see my struggle to make a prototype here

I am opening this to see what people think about this, I think this is mostly an edge case of mine, not sure if it is worth putting on a library

the-dr-lazy commented 5 years ago

Looks related to #66 (not exactly similar). The main issue is string interpolation which generalizes the string literal type to string type. Do you find any solution for it?

the-dr-lazy commented 5 years ago

There are some workarounds for string literal type operators in https://github.com/Microsoft/TypeScript/issues/12940 and https://github.com/microsoft/TypeScript/issues/6579.

LouizFC commented 5 years ago

My workaround would be passing an object with all the string literals like so:

//Over Simplified Version

// We have a "Factory" where we declare the shape of our ActionCreators
type Factory = {
  a: (name: string) => /** ActionCreator */ ()=> { type: name };
  b: (name: string) => /** ActionCreator */ ()=> { type: name };
};

// We have a object with the same keys as the factory where we pass the ActionTypes
type Names = {
  a: "A";
  b: "B";
};

// We have produce an object with the correct ActionCreators
type Result = {
  a: () =>{ type: "A" };
  b: () =>{ type: "B" };
};

// The shape of the function
function(factory): (names) => result

It introduces some boilerplate, but the types that it produces will have the correct literals.

LouizFC commented 5 years ago

I think we could tweak createActionCreator to return a cleaner type, but I don't know if this would be a breaking change.

Should I experiment with it in that same branch or create a separate branch?

I think that ithe ActionCreator type should have the following signature:

interface ActionCreator<
  TAction extends string,
  TCallable extends (...args: any) => any = (...args: any) => any,
  TPayload = undefined,
  TMeta = undefined
> {
  (...args: Parameters<TCallable>): Action<TAction, TPayload, TMeta>;
  type: TAction;
  toString(): TAction;
}
LouizFC commented 4 years ago

Sorry for being away. September was a busy month.

I will do a rewrite with the approach that you suggested soon as I get some time.

Spaubleit commented 3 years ago

Hello. Maybe it is possible to implement this using Template Literal Types from Typescript 4.1+

LouizFC commented 3 years ago

@Spaubleit Yes. At the time there was no recursive types nor template types on Typescript. I think implementing this should be quite easy at the current state. Thanks for bringing this issue to my attention

Right now I am using @reduxjs/toolkit slices, but a more strong typed alternative would be quite good

LouizFC commented 2 years ago

I am closing this because I see no more benefit in implementing this. As much as the action type information looks nice in the linter, its often wrong (I say from experience because I implemented this in a separate fork). Also, see https://phryneas.de/redux-typescript-no-discriminating-union