Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation/Recipe Request: Using redux-saga with TypeScript #1286

Closed
huntwj opened this issue Dec 5, 2017 · 23 comments
Closed

Documentation/Recipe Request: Using redux-saga with TypeScript #1286

huntwj opened this issue Dec 5, 2017 · 23 comments
Labels

Comments

@huntwj
Copy link

huntwj commented Dec 5, 2017

I see that there are type definitions, and some people have gotten them to work. As someone that's new to TypeScript, however, converting my redux-saga to TypeScript has gone less than smoothly.

It would be nice if we could add a page of documentation (preferably written by someone that has had success in doing this...) outlining the process of converting JS sagas to TS and/or writing TS sagas from scratch.

Just in case my issue turns out to be easily solvable by the experts, I'll include it here:

Specifically, I am attempting to use takeLatest(...) and call my API request sagas, but I am getting type errors on those that take parameters.

My TypeKeys:

export enum TypeKeys {
  FETCH_REQUEST = 'Fetch Request',
  .
  .
  .
}

In my watchers generator:

    yield takeLatest(TypeKeys.FETCH_REQUEST, fetchRequestSaga);

and in my sagas:

export function *fetchRequestSaga(getState?: () => any): SagaIterator {
  .
  .
  .
}

Then the error is:

TS2345: Argument of type 'TypeKeys.FETCH_REQUEST' is not assignable to parameter of type 'Channel<(() => any) | undefined>'.

I used to be able to call takeLatest passing in a string as the action type, but things seem to change now that I'm dealing with a TS (string-based) enum.

@Andarist Andarist added the docs label Dec 5, 2017
@Andarist
Copy link
Member

Andarist commented Dec 5, 2017

@aikoven do you know any good resources about the topic?

@aikoven
Copy link
Contributor

aikoven commented Dec 6, 2017

@huntwj The error you see is a result of TypeScript's way of resolving overloaded signatures. takeLatest has two kinds of signatures: one for taking actions and another for taking from channels (see effects.d.ts). For some reason TS failed to match your arguments with the first kind. Then it tried to match with the second kind, and of course it failed too, and returned an error for the last signature it tried.

Unfortunately, I don't know of a way to get the correct error message here, except for commenting out all the signatures of takeLatest with channels in node_modules/redux-saga/effects.d.ts.

You may also try the new typings for the 1.0 (#1255), where these two groups of signatures are swapped, so that users are more likely to get the correct error message, since takeLatest is more often used with actions than channels.

@erik-sn
Copy link

erik-sn commented Feb 22, 2018

@huntwj did you ever find a resolution for this?

@huntwj
Copy link
Author

huntwj commented Feb 22, 2018

I seem to have "moved past" the problems, though I cannot remember exactly what the solution was. I'm guessing it was related to using the 1.0 typings, but I can't remember off hand and unfortunately don't have time to do the necessary testing right now.

What I still think could be useful, and is something I may attempt to put together in the near future, would be a page of documentation providing a quick "getting started" guide for using redux saga with TypeScript.

@erik-sn
Copy link

erik-sn commented Feb 22, 2018

as a quick fix I just added a redux-saga/effects module in a custom .d.ts file and overrode the values, those are all good ideas though.

@AleksandrChernyavenko
Copy link

I had error Argument of type 'ChartActionTypes.FETCH_CHART' is not assignable to parameter of type 'Channel<{ payload: any; }>'.

So problem was in fetchChart(action: type) - it has wrong type. I added type for action, and this fixed error

export function* fetchChart(action: FetchChartAction) { // <-- here added type FetchChartAction
  const {error, data} = yield call(fetchChartData, action)
  if (!error) {
    yield put(fetchChartSuccess(data))
  } else {
    yield put(fetchChartFailure(error))
  }
}

export function* watchFetchChart() {
  yield takeLatest(ChartActionTypes.FETCH_CHART, fetchChart) // <-- here pass `action` argument
}

@agzamovr
Copy link

If above solution didn't work for you (as it didn't for me) try this one #1188 (comment)

@mynameistechno
Copy link

Are there any good examples/codebases that leverage redux-saga typescript types?

@gilbsgilbs
Copy link
Contributor

gilbsgilbs commented Apr 13, 2019

I've been using Redux-Saga and TypeScript at work in a medium-sized front-end codebase for a while. Overall, it works fine and I want to share the main issues, pitfalls and solutions I used. If you find it interesting, it could be a starting point for writing a recipe.

Setup

I defined a custom action interface that extends Redux's Action and that is inherited by all the actions I declare in my application. I think it is a best practice anyway. It somehow helps TypeScript at inferring types and avoids some explicit type casts:

import { Action } from 'redux';

interface AppAction extends Action { 
    payload?: any;
}

interface LogAction extends AppAction {
  type: 'LOG_ACTION';
  payload: string;
}

Since TypeScript can guarantee there's no other AppAction having LOG_ACTION as type, you can then write:

yield RSEffects.takeEvery(
  (action: AppAction) => (
    action.type === 'LOG_ACTION'
    && action.payload === 'hello'
  ),
  someWorker,
);

instead of:

yield RSEffects.takeEvery(
  (action: Action) => (
    action.type === 'LOG_ACTION'
    && (action as LogAction).payload === 'hello'
  ),
  someWorker,
);

(there might be more insightful examples though)

The store can be instantiated normally :

const sagaMiddleware = createSagaMiddleware();
const store = Redux.createStore(
    reducer,
    Redux.applyMiddleware(sagaMiddleware),
);
sagaMiddleware.run(rootSaga);

Sagas

The simplest way to create a saga is to create a generator having SagaIterator as return type:

import { SagaIterator } from '@redux-saga/core';
import { delay } from '@redux-saga/core/effects';

function* saga(): SagaIterator {
  yield delay(1000);
  console.log('ok');
}

Effects can be used as expected:

function* saga(): SagaIterator {
    const chan = ReduxSaga.channel();  // <== "Channel" type inferred
    chan.put('hello');
    const res: string = yield RSEffects.take(chan);  // <== can take from a channel, no problem
    console.log(res);

    yield RSEffects.takeEvery(
        'LOG_ACTION',  // <== can take from an action type string
        function* (action: LogAction): SagaIterator {
            console.log(action);
        },
    );
}

The Saga type can be used to reference sagas (sorry, I don't have a better example that comes into my mind yet, but I know I had to use the Saga type from time to time, so I think it's worth mentioning):

import { Saga, SagaIterator } from '@redux-saga/core';
import { call, delay } from '@redux-saga/core/effects';

function* saga(subSaga: Saga): SagaIterator {
  yield delay(1000);
  yield call(subSaga);
}

Return types / Next types

This is THE major painpoint when working with Redux-Saga and TypeScript: the lack of support for generators return types (this will be fixed in TS 3.6) and not flexible next() typing for generators (cannot find a proper issue to track this). The only workaround I know is to type effects return type explicitly:

function* saga(subSaga: Saga): SagaIterator {
  const response: Response = yield RSEffects.call(fetch, 'https://httpstat.us/200');  // <== Typed explicitely, otherwise we have "any"
  const txt: string = yield RSEffects.call([response, 'text']); // <== Same
  console.log(txt);
}

Generic types and Sagas

This is the less straightforward part I had to deal with. TypeScript seems pretty bad at inferring generic types on generator functions. It may work in simple cases, but very often, TypeScript will report some typing errors when using the call effect on a Saga with generic types, or even worse, report no error at all even though typing is obviously incorrect. I still have not sorted out in which extent this is a TypeScript bug and/or limitation and/or Redux-Saga having improper typings. Still it bit me a few times.

Example:

function* genericSaga<
  T extends (str: string) => number
>(callback: T): SagaIterator {
    yield RSEffects.delay(1000);
    console.log(callback('hello'));
}

function* saga: SagaIterator {
    yield RSEffects.call(genericSaga, 42); // No error! WTF?
}

This is an example where TypeScript reports no error at all. Conversely, I had more complex cases that I failed to simplify where TypeScript would be unable to infer a type. And it's really unpleasant to try to give an explicit generic type to call. Luckily, it's not a very common use case. It's mainly useful for low-level primitives (e.g.: task queue that takes a worker as input parameter).

When working on this problem, I came across a workaround that I found elegant and I think is worth sharing. The solution was to refactor saga generators into plain effect functions. I found this pattern to be much convenient to use since it hides the call primitive to the calling saga. If there's no gotchas I missed, I think this pattern could be a recipe for untyped Redux-Saga and could be used consistently when declaring a saga that may be called by other sagas.

function genericEffect<
  T extends (str: string) => number
>(callback: T): CallEffect {
    return call(function*(): SagaIterator {
        yield RSEffects.delay(1000);
        console.log(callback('hello'));
    });
}

function* saga: SagaIterator {
    yield genericEffect(42); // Error: 42 not assignable to Fn. This is expected.
    yield genericEffect((str) => str.length); // OK
}

Hope this helps. What do you think? Did I miss something important? Can it serve as a base for writing a recipe? Did I make any nasty mistake? If you have any question, do no hesitate.

@mynameistechno
Copy link

This is super helpful, thanks!

@nithincvpoyyil
Copy link

nithincvpoyyil commented Jun 14, 2019

[SOLVED]

Hi All,

I`m getting an error for following code,

just followed examples.

const stack =[];
const generator = await runSaga({
      dispatch: (actionDispatched:any)=>{stack.push(actionDispatched)},
      getState: ()=>({data:[]})},
    }, fetchDataSaga).toPromise();

'await' expression is only allowed within an async function.ts(1308)'

Currently I am doing like this,

const generatorFun = async ()=>{
return runSaga({
      dispatch: (actionDispatched:any)=>{stack.push(actionDispatched)},
      getState: ()=>({data:[]})},
    }, fetchDataSaga).toPromise();
};

await genetaroFun();

Any suggestions ? thanks in advance.!!

[UPDATE] : please see the comment below by @gilbsgilbs

 it('test abc', async () => {

 const stack =[];
 const generator = await runSaga({
      dispatch: (actionDispatched:any)=>{stack.push(actionDispatched)},
      getState: ()=>({data:[]})},
    }, fetchDataSaga).toPromise();
 });

@gilbsgilbs
Copy link
Contributor

@nithincvpoyyil this is not a typing / typescript issue. As indicated, you need to find a way to use await keyword from an async function. If using jest, you can just make your test function async like this : https://jestjs.io/docs/en/asynchronous.html#async-await .

@nithincvpoyyil
Copy link

nithincvpoyyil commented Jun 14, 2019

@gilbsgilbs : Thank u for the response. Yes, that was the issue. jest test function should be async, I will add it as a comment above. feeling so stupid..

@jtomaszewski
Copy link

jtomaszewski commented Jul 7, 2019

Seems like strong typing of generators is finally gonna come in TS 3.6? microsoft/TypeScript#31639 Which means, we will be able to have well typed sagas using generic types? (Because it's not really possible atm, am I correct?)

Are there some plans/work towards supporting it?

@Andarist
Copy link
Member

Andarist commented Jul 7, 2019

Yes, when TS@3.6 gets released we are definitely going to work on improved typings.

@gilbsgilbs
Copy link
Contributor

gilbsgilbs commented Jul 7, 2019

Not willing to spoil the party or anything, but microsoft/TypeScript#31639 isn't actually sufficient to strongly type sagas and other co-style libraries as we would like. While it is indeed a slight improvement since we're now able to typecheck sagas return value:

function* mySaga(): SagaIterator<number> {
  yield call(someEffect);
  return "not a number";  // This will fail
}

and even the yield assignment/next to some extent:

function* mySaga(): SagaIterator<void, number> {
  const val = yield call(someEffect);  // val will have "number" type
}

it remains too dumb to properly infer types in such cases:

function* mySaga(): SagaIterator {
  return "not a number";  // This generator should have `string` ReturnType properly infered
}

function* anotherSaga(): SagaIterator {
  const val = yield call(mySaga);  // val will still have `any` type :(
}

And as far as I understand, there's nothing we can do about it at the moment. I'm looking for the actual issue to follow on TypeScript, but I'm unsure.

This isn't really related to this issue though. Maybe we could keep track of this in another issue @Andarist?

See microsoft/TypeScript#30790 (comment) and microsoft/TypeScript#2983 (comment) for more information.

@gilbsgilbs
Copy link
Contributor

I think one approach to improve the situation a bit would be to write typescript-eslint rules crafted for Saga that would for example ensure that:

  • Every yield assignment is explicitly typed
  • If the yielded effect is a CallEffect, ensure that the yield assignment type matches the return type of the CallEffect
  • If the yield effect is a ForkEffect, ensure that the yield assignment type is a Task with a proper generic type
  • etc…

And generator return types could probably help to implement this more easily:

function* childSaga(): SagaIterator<number> {
  yield delay(100);
  return 12;
}

function childCallEffect(): CallEffect<number> {
  return call(childSaga);
}

// until here, TS 3.6 should be enough to strongly type everything.
// starting from here, the linting rule would come into place

function* parentSaga(): SagaIterator<void> {
  // typeof call(childSaga) is CallEffect<number> => ensure we assign number here.
  const childSagaResult: number = yield call(childSaga);

  // And similarly
  const childCallEffectResult: number = yield childCallEffect();

  // typeof call(fetch<any>(…)) is CallEffect<Response<any>> => ensure we assign a Response<any> here.
  const reqResult: Response<any> = yield call(fetch<any>('http://example.com'));
}

I'm very unsure how realistic this idea is in term of complexity or even feasibility. Yet, some typescript-eslint rules are smart enough to leverage type information. Therefore, I believe it's feasible at very least, maybe just not worth the complexity.

The thing is I'm quite pessimistic on the ability of the TS team to come up with a general solution to this issue anytime soon. And I don't blame them, the general case seems very very hard to tackle after thinking of it a bit. Still, it may be more worth than we think to provide a very imperfect solution.

What do you think?

@Andarist
Copy link
Member

If it's possible then it would be great if somebody could work on it - I'm afraid the effort would be community-driven though, as personally I don't have time to explore this right now.

@gilbsgilbs
Copy link
Contributor

I made some experiments tonight and it appears to be feasible. I updated redux-saga types to allow specifying a return type to saga generators (using TS 3.6 generator return types) and I am able to get this return type within an ESLint rule 🎉 . I may open a PR sometime for Sagas return types and if it makes it, I may release an ESLint plugin to enforce strong types on effects. We could then write a good Typescript recipe :) .

@Andarist
Copy link
Member

Great, looking forward to it.

@dwilt
Copy link

dwilt commented Aug 16, 2019

@gilbsgilbs any progress on this?

@gilbsgilbs
Copy link
Contributor

@dwilt progress on the ESLint rule? I don't think I'll spend any time on it because somebody found a simpler solution that would cope well with type inference (at the price of slight changes in the way sagas are invoked). If our community don't fall into pointless quarrels waged by ego, I think it might be the definitive solution to strongly typed sagas.

About the improved typings for redux-saga with TS3.6 (which I think is a prerequisite for all the solutions we have), I made a WIP PR (#1892) however, as I mentioned, I'm stuck and I need to figure out why typesVersions doesn't work as I would expect. Not sure when I will find enough patience and time to dig into this, but probably not soon since I'm very prone to rage quitting on packaging-related topics 😓 . If you're willing to help making this happen sooner rather than later, I think this issue is the very next step 😉 .

mergify bot pushed a commit to celo-org/celo-monorepo that referenced this issue Jul 23, 2020
### Description

There was a typo in `requesterAddrress` in the code handling QR codes in secure send mode.
Not sure exactly of the exact problem it would cause, I'll let @tarikbellamine comment.

Long term fix is to find a solution for fully typed `redux-saga`.
As of today, there's no clear solution to this, see more discussion redux-saga/redux-saga#1286

### Other changes

- Added manual types for `yield take(Actions...)`.

### Tested

TypeScript check succeeds.

### Backwards compatibility

Yes.
@neurosnap
Copy link
Member

I’d like to overhaul the docs to emphasize Typescript in the near future. Currently we are blocked by TS itself providing better support for yield overrides. You can track that progress here: microsoft/TypeScript#43632

Going to close this issue for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests