-
Notifications
You must be signed in to change notification settings - Fork 122
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
Make Cmd.run typesafe #222
Conversation
@@ -96,6 +96,9 @@ declare namespace Cmd { | |||
export const dispatch: symbol; | |||
export const getState: symbol; | |||
export const none: NoneCmd; | |||
export type Dispatch = <A extends Action>(a: A) => 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.
dispatch technically returns a promise
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.
A promise of the action or void, ? <A extends Action>(a: A) => Promise<A>
. The redux typing returns just the action:
export interface Dispatch<A extends Action = AnyAction> {
<T extends A>(action: T): T
}
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.
Why do we define and export Dispatch
in here at all? Let's just import it from Redux.
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.
@domarmstrong @laszlopandy any thoughts on this? is the reason we don't just reuse the redux dispatch type because the return type is different?
and @domarmstrong i think i forgot to respond to your original comment months ago. it actually returns a promise with the new state object.
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 think we should have a different type because the Redux type definitions show that dispatch returns an action, but in redux-loop we return a Promise<Action>
. I think this warrants making a new type, but yes we should be consistent about this.
Also I never understood why dispatch would return anything in redux. If someone knows, I would be glad to understand it.
Is any of this using a new typescript feature that requires a version we don't currently require, or is it a breaking change at all? I think we currently require at least 2.8, but I'm not too familiar with typescript as I don't use it myself. |
index.d.ts
Outdated
@@ -115,16 +118,30 @@ declare namespace Cmd { | |||
args?: any[] | |||
): MapCmd<A>; | |||
|
|||
export function run<A extends Action, B extends Action>( | |||
f: Function, | |||
type InferArgs<T> = T extends (...args: infer T) => any ? T : 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.
what exactly does infer do here?
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 basically says from this type this is the argument I want to infer my variable T to.
Its a conditional type that only matches the left hand size, its a bit of a quirky typescript syntax.
Simplified examples:
type NumberName<T> = T extends number ? 'number' : 'notNumber';
type N = NumberName<2>; // 'number' < literal
type S = NumberName<'a'>; // 'notNumber' < literal
Then add never:
type NumberName<T> = T extends number ? 'number' : never;
type N = NumberName<2>; // 'number' < literal
type S = NumberName<'a'>; // never;
Then add infer;
type Args<T> = T extends (...args: infer A) => any ? A : never;
type A = Args<(a: number, b?: string) => void>; // [number, (string | undefined)]
Hopefully that makes sense?
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.
Is this not the same as TypeScript's built-in Parameters
?
See lib.es5.d.ts:
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : 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.
Yes, thanks. I've updated to use Parameters
Yes it does require ^3.0.1 because of the generic rest parameter https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#generic-rest-parameters. I don't think its possible to infer the argument before that, but it is one of the more useful bits. The version included in the package is ^3.2.2 though ¯_(ツ)_/¯ |
[K in keyof T]: T[K] extends Dispatch | GetState | ||
? typeof dispatch | typeof getState | T[K] | ||
: T[K]; | ||
} |
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.
To be honest I didn't think that the type system could full express the Cmd.run
API! I'm glad to learn that TypeScript is this powerful, but if we have to resort to this much magic, maybe we should revisit this API and see if it can be simplified first.
@laszlopandy can you review this? Since it requires typescript v3, it sounds like it'll be a breaking change to introduce. How widespread is typescript v3 usage? Is it a difficult upgrade from v2? |
TS Upgrades are generally smooth, and v3 was released several months ago, so I don't anticipate updating to TS v3 to be a major issue for people. We have been updating to every minor version sequentially in <1 day over the last two years. But if the API significantly changes to make it easier to typecheck that can be a larger breaking change and a hurdle towards upgrading. I think it would be good if this change is merged and any API changes (if needed) be taken up in separate breaking changes. |
@lorefnon awesome. Then I think we can just do a v6 to release this. There is another breaking change I want to release anyway |
index.d.ts
Outdated
failActionCreator?: ActionCreator<A>; | ||
successActionCreator?: ActionCreator<B>; | ||
args?: ArgOrSymbol<Parameters<Func>>; | ||
failActionCreator?: (error: Err) => FailAction; |
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.
A promise can fail with error: any
. It is not always an Error
. To be honest, I would leave this as any
. In TypeScript Promises cannot specify the error type, so it might not be worth it to specify an error type here.
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've updated this, it seems sensible
Is this still true? Does this mean if you switch the arguments accidentally it doesn't give a type error? |
No, I've fixed this. It should correctly error now if you use the wrong symbol, which I've tested in the loopReducer.ts file. - [K in keyof T]: T[K] extends Dispatch | GetState
- ? typeof dispatch | typeof getState | T[K]
- : T[K];
+ [K in keyof T]: T[K] extends GetState
+ ? typeof getState
+ : T[K] extends Dispatch
+ ? typeof dispatch
+ : T[K]; |
Thanks @domarmstrong in that case I suggest we merge this now and deal with any new issues when we prepare for the next release. Thanks for your work on this. (FYI: it's not necessary to force push to keep a single clean commit; we squash merge PRs in this repo). |
I notice there is a stale PR that tackles the same problem.
This PR goes a step further with argument inference while still allowing the special symbols to be used if the fn passed has arguments that would match the types of Dispatch or GetState (although I can't distinguish between them as the type symbol === symbol)