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

Make Cmd.run typesafe #222

Merged
merged 1 commit into from
Apr 17, 2020
Merged

Make Cmd.run typesafe #222

merged 1 commit into from
Apr 17, 2020

Conversation

domarmstrong
Copy link
Contributor

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)

@@ -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;
Copy link
Member

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

Copy link
Contributor Author

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
}

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor

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.

@bdwain
Copy link
Member

bdwain commented Aug 18, 2019

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;
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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;

Copy link
Contributor Author

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

@domarmstrong
Copy link
Contributor Author

domarmstrong commented Aug 28, 2019

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.

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];
}
Copy link
Contributor

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.

@bdwain
Copy link
Member

bdwain commented Feb 25, 2020

@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?

@lorefnon
Copy link

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.

@bdwain
Copy link
Member

bdwain commented Mar 26, 2020

@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;
Copy link
Contributor

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.

Copy link
Contributor Author

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

@laszlopandy
Copy link
Contributor

@domarmstrong

(although I can't distinguish between them as the type symbol === symbol)

Is this still true? Does this mean if you switch the arguments accidentally it doesn't give a type error?

@domarmstrong
Copy link
Contributor Author

@domarmstrong

(although I can't distinguish between them as the type symbol === symbol)

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];

@laszlopandy
Copy link
Contributor

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).

@laszlopandy laszlopandy merged commit 3ecf576 into redux-loop:master Apr 17, 2020
index.d.ts Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants