-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Adds return type parameter to Saga #2295
Conversation
🦋 Changeset detectedLatest commit: 2d21a2e The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Sorry for the double-ping @Andarist and @neurosnap, wasn't sure how to add reviewers. Thanks in advance for reviewing this 🙏 |
@@ -1,6 +1,6 @@ | |||
import { Action } from 'redux' | |||
|
|||
export type Saga<Args extends any[] = any[]> = (...args: Args) => Iterator<any> | |||
export type Saga<Args extends any[] = any[], TReturn = any> = (...args: Args) => Iterator<any, TReturn> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note for me (and other reviewers) - this change is unique to 3.6+ types because previous versions of the Iterator
didn't accept more than one generic.
Could you add type-level tests for this change? |
👍 Done. As part of doing that I also updated the types for |
What do you think @Andarist? |
packages/core/types/ts3.6/index.d.ts
Outdated
@@ -166,7 +166,7 @@ export interface SagaMiddleware<C extends object = {}> extends Middleware { | |||
* @param saga a Generator function | |||
* @param args arguments to be provided to `saga` | |||
*/ | |||
run<S extends Saga>(saga: S, ...args: Parameters<S>): Task | |||
run<Args extends any[], TReturn>(saga: Saga<Args, TReturn>, ...args: Args): Task<TReturn> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a change in the declared signature. What does it give us? Can you come up with any real-life scenario?
I know some stuff about the difference between the two but I'd like to hear out your thoughts and motivation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a change, but given it's just changing inferred generic type parameters, I don't think it's a breaking change. I don't think it's common for people to specify the saga's type, they just pass the saga.
I'm not sure what kind of description or specific use cases you're looking for beyond the example I gave in the PR description, and of course the tests showing that the task's type was not being inferred before.
In my application I have a function runSaga that takes a saga as a parameter and returns the Task that the saga returns when it's begun. I want to get the proper type of the Task's result, but am not able to do so with the current Saga type definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please let me know if I can be clearer or if you think the tests don't demonstrate the improvement — getting a specific, inferred type for the task's return instead of any
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But does it provide a meaningful improvement anywhere? Can we add a test that would "prove" that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused, are you not seeing the two tests that are a part of this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see them but I don't see from them how exactly changing the generic signatures of run
/runSaga
have made them better.
What I mean is that to make them work it would probably be enough to just extract the return type like this:
type SagaReturnType<S extends Saga<any, any>> = S extends Saga<any, infer R> ? R : never
// ...
run<S extends Saga>(saga: S, ...args: Parameters<S>): Task<SagaReturnType<S>>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see — you understand the point and value of adding the return type to Saga
, but you have a way you would prefer to implement this piece of it.
I don't have a preference between the two forms, the way I wrote it was just how I'd figured out how to do it.
Went ahead and changed it to your suggested form 👍
export type Saga<Args extends any[] = any[]> = (...args: Args) => Iterator<any> | ||
export type Saga<Args extends any[] = any[], TReturn = any> = (...args: Args) => Iterator<any, TReturn> | ||
|
||
export type SagaReturnType<S extends Saga<any, any>> = S extends Saga<any, infer R> ? R : never |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a case in which we might get never
back from this. I don't see any particular problems with this... but it's fine somewhat coincidentally. It's fine because the computed return type is only used within toPromise
and result
.
It's fine for toPromise
as a never-returning iterator should, well, never complete - so the returned promise won't ever resolve and thus it doesn't quite matter.
It's fine for result
because T
is there included within a union with undefined
, never
is always eliminated from unions, so the only leftover will always be just undefined
in such a case and that's correct.
This TS playground might show what kind of edge cases I was thinking about while reviewing this change.
I just wonder if we should have any tests to check against potential regressions here. 🤔 Maybe I'm overthinking this though.
It's also worth noting that T
in Task
is covariant and thus Saga<any, never>
satisfies Saga<any, any>
. It's always worth re-checking this because the same is not true for contravariant parameters: TS playground
@@ -7,7 +7,8 @@ declare const effect: StrictEffect | |||
declare const iterator: Iterator<any> | |||
|
|||
function testRunSaga() { | |||
const task0: Task = runSaga<{ foo: string }, { baz: boolean }, () => SagaIterator>( | |||
// $ExpectType Task<void> | |||
const task0 = runSaga<{ foo: string }, { baz: boolean }, Saga<undefined[], void>>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like you might have wanted to just provide no arguments:
const task0 = runSaga<{ foo: string }, { baz: boolean }, Saga<undefined[], void>>( | |
const task0 = runSaga<{ foo: string }, { baz: boolean }, Saga<[], void>>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried that and got this error:
ERROR: 11:60 expect Compile error in typescript@3.7 but not in typescript@3.8.
Fix with a comment '// Minimum TypeScript Version: 3.8' just under the header.
Type 'Saga<[], void>' does not satisfy the constraint 'Saga<any[], any>'.
Type 'any[]' is not assignable to type '[]'.
Types of property 'length' are incompatible.
Type 'number' is not assignable to type '0'.
So left as-is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, let's just wait a day or two to see if @neurosnap has any additional thoughts on this one before merging this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for diving into this @Andarist -- I'm happy with the changes proposed here.
@lourd We are ready to merge, we just need a changeset. Could you please create one for this PR? Thanks! |
@neurosnap can you point me towards directions on how to do that? I tried before but couldn't figure it out |
If you run yarn changeset it should walk you through the process. Also feel free to read more about it here https://github.com/changesets/changesets |
In my application I have a function
runSaga
that takes a saga as a parameter and returns theTask
that the saga returns when it's begun. I want to get the proper type of the Task's result, but am not able to do so with the currentSaga
type definition.By adding
TReturn
toSaga
and makingTask
accept its return type as a parameter,result
will be inferred correctlyHere's a TypeScript playground I was using to play with the types
This is a non-breaking change since it's optional with a default of
any
.The use case is effectively dependent on #2241, but these changes can be reviewed and merged separately.