-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
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
feat: improve helper types for more type safety #1121
Conversation
types/index.d.ts
Outdated
type: string; | ||
} | ||
|
||
export interface MutationPayload extends Payload { | ||
type Payload<K extends keyof P, P> = { type: K } & P[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.
Technically this is a breaking change. I don't know how frequent Payload
is used, but this might be a compatible alternative:
type Payload<P = { [k: string]: {} }, K extends keyof P = keyof P> =
{ type: K } & P[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.
I ended up reverting the rename of Payload
0858c6d
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, changing interface
to type
would be a breaking change. My mistake 😷
Thanks! This is a long change so I think I need some time to review the change. Also, we need documentation update to catch up this new awesomeness. 😄 |
types/helpers.d.ts
Outdated
type Computed<R> = () => R; | ||
type Method<R> = (...args: any[]) => R; | ||
type MutationMethod<P> = (payload: P) => void; | ||
type ActionMethod<P> = (payload: P) => Promise<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.
I'm fine with untyped action return type. In fact, users can register multiple actions so the return type can't be checked any way.
types/helpers.d.ts
Outdated
* mapMutations | ||
*/ | ||
interface MapMutations<Mutations> { | ||
<M extends Mutations = Mutations, Key extends keyof M = keyof M>(map: Key[]): { [K in Key]: MutationMethod<M[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.
Usually generic default is for interface / type alias. Mapper is not exported nor augmented by users, so I think these defaults aren't required.
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.
Actually, it throws error if Mutations
is BaseType
since M
will be inferred as {}
in that case. I have no idea how we fix it without default generic type 😞
But we may no longer need it if we create typed root helpers with createNamespacedHelpers
in any cases.
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.
Let's try typed root helpers! It looks more promising since it can return more precise types.
& MapperWithNamespace<Computed> | ||
& MapperForState | ||
& MapperForStateWithNamespace; | ||
export declare const mapState: RootMapState<BaseType, BaseType>; |
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.
The proposed usage is mapState<RootState>(['foo', 'bar'])
, however, the return type is RootState
because foo/bar
isn't participating inference.
Another usage is that we export RootMapXXX
and slightly change their type.
export interface RootMapState<State> {
<Keys extends keyof State>(keys: Keys[]): {[K in Keys]: State[K]}
}
then users can write something like const myMapState: RootMapState<RootState> = mapState
. Of course, it requires us to maintain more public types, and also looks a little bit bizarre to end users.
But it is more versatile in usage -- I think object style can be supported, and more precise in returning type.
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.
How about reusing the pattern of createNameSpacedHelpers
? Adding one more helper function like createRootHelpers
? Implementation wise, it just return these root helpers, but it gives better developer experience for both end users and lib maintainers.
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.
How about reusing the pattern of createNameSpacedHelpers
That sounds good idea. I think returning root helpers from createNamespacedHelpers
with no argument would work. createRootHelpers
would sound a bit weird for pure JS users.
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.
Awesome! Great balance between types and runtime behavior.
* `ExtraGetters` is like `Getters` type but will be not defined in the infered getters object. | ||
* `RootState` and `RootGetters` are the root module's state and getters type. | ||
*/ | ||
export type DefineGetters< |
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 about a one-for-all option? like :
export type DefineStore<
Actions,
State,
Getters,
Mutations,
ExtraActions = {},
RootState = {},
RootGetters = {},
RootMutations = {},
RootActions = {}
> = {
// ....
}
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, it looks better since we always combine all assets for module 😄
I will rewrite the utility types.
I will write docs for this. Thanks for pointing it out! |
types/helpers.d.ts
Outdated
* mapMutations | ||
*/ | ||
interface MapMutations<Mutations> { | ||
<M extends Mutations = Mutations, Key extends keyof M = keyof M>(map: Key[]): { [K in Key]: MutationMethod<M[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.
There is a problem with returning MutationMethod
here.
MutationMethod
does not have ...args: any[]
parameters anymore.
This makes all mutations take one argument, same goes for actions.
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's not actually a problem because mutations and actions only accept one parameter. The returned methods will never process 2nd or latter parameters.
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 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.
You can pass multiple parameters (or none) to mapped actions
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. It should be an optional argument in default to follow the existing behavior.
But I think if we annotate types via createNamespacedHelper
explicitly, it should not be an optional. Because it easily breaks apps if the user adds a payload for actions/mutations with no payload and the type checker cannot detect that.
If the users want to declare non-payload actions/mutations, they should explicitly pass a null
or undefined
value.
@HerringtonDarkholme What do you think?
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 agree that users should explicitly pass null
or undefined
.
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 agree, i believe undefined
is probably best to signal that this payload is void. null
could have context to an application where undefined can't be used on the store as it breaks the reactivity principles.
types/helpers.d.ts
Outdated
<Key extends keyof Mutations>(map: Key[]): { [K in Key]: MutationMethod<Mutations[K]> }; | ||
<Map extends Record<string, keyof Mutations>>(map: Map): { [K in keyof Map]: MutationMethod<Mutations[Map[K]]> }; | ||
interface MapMutations<Mutations, Type extends MethodType> { | ||
<Key extends keyof Mutations>(map: Key[]): { [K in Key]: MutationMethod<Mutations[K], Type> }; |
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 typing still requires all methods in mutation/action are homogeneous: all methods either require one parameter at the same time or don't accept parameter at all. We cannot declare such mutations that some methods require parameter while others don't at the same time.
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, I intend that behavior. I leave them optional if the users do not annotate types because they probably want flexible syntax like in JS. On the other hand, the methods always require an argument if they annotate types because they probably want type safety in that case.
docs/en/api.md
Outdated
|
||
Create namespaced component binding helpers. The returned object contains `mapState`, `mapGetters`, `mapActions` and `mapMutations` that are bound with the given namespace. [Details](modules.md#binding-helpers-with-namespace) | ||
|
||
If the namespace is not specified, it returns the root mapXXX helpers. This behavior is convenient to annotate strict types for mapXXX helpers. |
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.
Hmmm, what about
This is mainly for TypeScript users to annotate root helper's type.
Mentioning TypeScript explicitly makes JS users know annotating type doesn't require much care for them.
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 sounds clearer than before. Thanks!
types/index.d.ts
Outdated
@@ -12,9 +12,7 @@ export { | |||
} from "./helpers"; | |||
|
|||
export { | |||
DefineGetters, |
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.
IMHO it doesn't bother if we re-export other utility type helpers. These helper types can be convenient for users to separate getters/actions/mutations to different files.
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.
Makes sense. I've exposed them again.
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.
As we are doing a large change for TS users, we should probably do this aswell: #994
So that $store
can be argumented, thus giving more type safety.
any ETA when this will be released? Currently I have to patch my ts code with some explicit 'any' to make it work which isn't ideal.. :< |
Great Work! Question on the usage. Without doing
|
Please, resolve the conflicts |
Within an action "this" refers to the store instance. Can we modify DefineActions type like below?
|
What's holding up this PR from being merged? Can anyone tell me what a good workaround is, in the mean time? I'm not that fluent in TypeScript. |
I too would love this PR to get merged. |
Looks like @yyx990803 is yet to review this PR. |
It will make strong sense for this PR been merged. |
strill waiting for this pr |
Every maintainer of this repo seems to have forgotten about all type-improving pull requests... |
@ktsn Any updates on this? I wrote a Vue + TS cookbook, and provide a workaround for this problem: But it would be nice if it worked properly out of the box. |
Could someone please merge this? @yyx990803 @blake-newman @HerringtonDarkholme |
I think this PR will not be ever merged, because TypeScript support with breaking changes is announced to be in VueJs 3 https://medium.com/the-vue-point/plans-for-the-next-iteration-of-vue-js-777ffea6fabf |
@kirilvit That's a great point, I totally didn't think of that. |
But at least a statement would be fair, instead of just saying nothing... |
any progress? |
Made a PR ktsn#1 updating Typescript@3.3.3 |
Vue 3 is still a very long ways away, right? If the maintainer doesn't want to merge this, could they close it and explain why? Or suggest what changes need to be made to get this merged in? |
I think this can't be merge because doesn't work on typescript 3.x (building errors) |
This is a little unrelated, but I've also create new typings for vuex. Similar to this, except instead of 4 interfaces to represent the state/getters/mutations/actions you specify one and it automatically can detect which properties are each: https://github.com/ClickerMonkey/vuex-typescript-interface |
@ktsn Do you think this PR is still maintainable...? Or maybe we should close this one and start over if we really need it 😓 |
We should go with Vuex 5's TypeScript improvement. Closing. |
This typings update allows to use typed getters/actions/mutations out of the box if they are used in the following manner.
1. Declare each store assets types as interfaces.
2. annotate a namespaced module with
DefineModule
utility type.The annotated assets must fulfill specified names and return type (getters) / payload type (actions, mutations). Also the assets types will be inferred.
The type in the following example should be fully inferred:
3. create typed namespaced helpers with
createNamespacedHelpers
.Then, we can acquire typed mapXXX helpers for the defined namespaced module.
4. use the namespaced helpers in a component.
Caveats
Store is still not typed
I think it is probably impossible to infer the entire store type correctly since we cannot concat getter/actions/mutations names with namespace on type level. So this PR focuses how we do not use
$store
directly but use typed helpers instead.It does not infer the types completely if passing functions to mapXXX helpers.
For example:
We can write the above code with inferred
state
andcommit
types but the component will have a type offoo: any
andbar: (...args: any[]) => any
.It can be easily rewrite with a combination of object form mapXXX helpers and normal methods, so I think it would not be a problem.
Using root assets
The default mapXXX helpers still accepts any asset names and returns record of
any
type. To manually annotate them, we can usecreateNamespacedHelpers
too. If we don't specify a namespace as an argument, it returns the root mapXXX helpers so that we annotate with root module assets types as same as namespaced modules.fix #532
fix #564
fix #1119