agiledigital / typed-redux-saga

An attempt to bring better TypeScript typing to redux-saga.
MIT License
315 stars 33 forks source link

Suggestion: Add action sensitive type effects #144

Open psacawa opened 4 years ago

psacawa commented 4 years ago

Hello, and thanks for this library that solved a long issue of mine with redux-saga. I would like to suggest improved typing for effects that yield actions, i.e. take.

const action = yield* take("ADD_FOO");
// inferred type of action is Action<any>

With an expression like above, the inferred type for action is very imprecise. If we want to safely use meta or payload of the action in the saga, we must still assert its type. Some libraries like redux-toolkit and typesafe-actions suggest precisely typing action creators and using them to power generic type inference in e.g. reducers. This could also be done with redux-sagas, using the workaround of this package. I'm suggesting adding a typesafe take effect that take an action creator instead of the action type, as below:

export function typedTake<AC extends (...args: any[]) => any>(
  pattern?: AC
): SagaGenerator<ReturnType<AC>, never>;
export function* typedTake(args: any) {
  return yield rawTake(args.type);  // uses redux-toolkit actions
}

const addTodo = createAction<string>("ADD_TODO");
// from redux-toolkit; addTodo('some text').payload has type string
// and the type is attached as addTodo.type
function* todoSaga() {
  const action = yield* typedTake(addTodo);
  // type correctly inferred
}

The above is not specific to any kind of action creator except that type attribute of action is passed to rawTake. Overloading take from this library is not possible because redux-saga already interprets a function argument as a filter. The above can be extended to arrays of action creators to mirror redux-saga.

I am happy to hear any thoughts.

danielnixon commented 4 years ago

If I understand correctly, you can achieve this with take as-is by providing a type argument:

  // All your app's action types
  type FooAction = { type: "FOO" };
  type BarAction = { type: "BAR" };
  type MyAction = FooAction | BarAction;

  // A version of `take` constrained to your action type(s).
  const myTake = take<MyAction>;

  // Usage
  yield* myTake("FOO"); // returns a FooAction
  yield* myTake("BAR"); // returns a BarAction
  yield* myTake("BAZ"); // doesn't compile
psacawa commented 4 years ago

Thank you, this would work. With redux-toolkit I am able to narrow the type literal from string to make this work. However, in Typescript 3.9.7 I get a syntax error on const myTake = take<MyAction>; It expects the function/generator to be called. How are you able to bind the type parameter in generic function without evaluating?

danielnixon commented 4 years ago

Ah, you'll have to do something like:

const myTake = <A extends MyAction>(pattern: A["type"] | A["type"][]) => take(pattern);
danielnixon commented 4 years ago

I'd be happy to explore improving the ergonomics of this. I can imagine this package providing something like:

const { take, ... } = typedEffects<MyAction>();

  yield* take("FOO"); // returns a FooAction
  yield* take("BAR"); // returns a BarAction
  yield* take("BAZ"); // doesn't compile

A PR for this hypothetical typedEffects is welcome. :)

psacawa commented 4 years ago

I've thought a bit about what would be a perfect typing experience for redux-saga. It would have the following properties:

import { take, ... } from 'typed-redux-saga'
const addTodo = createAction<{text: string}, 'ADD_TODO'>('ADD_TODO')
const removeTodo = createAction<number, 'REMOVE_TODO'>('REMOVE_TODO')
// how you get a strictly typed action creator from redux-toolkit

function* saga () {
  const action1 = yield* take(addTodo)
  const action2 = yield* take([addTodo, removeTodo])
  // type inferred
}

The mixed case of yield* take(addTodo, 'REMOVE_TODO') is probably less important.

I will generate something like this for my own work, and try to make a PR.

danielnixon commented 3 years ago

I'm experimenting with this in the https://github.com/agiledigital/typed-redux-saga/tree/effects-for-action-type branch