-
-
Notifications
You must be signed in to change notification settings - Fork 9.5k
feat: improve helper types for more type safety #1121
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
Changes from 11 commits
0768c80
25775c5
21ee399
eee1f3c
f94cf70
a5c4e26
58d28a5
0858c6d
1e27c5e
c2068f3
7abf34f
9564b80
1aa407f
cfb6042
00360b5
9b89ae7
09475d5
8e0c60b
b14662c
b24d744
8b6a6f9
9b183f0
715eaad
fc1f29b
c3626f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,69 +1,118 @@ | ||
| import Vue from 'vue'; | ||
| import { Dispatch, Commit } from './index'; | ||
|
|
||
| type Dictionary<T> = { [key: string]: T }; | ||
| type Computed = () => any; | ||
| type MutationMethod = (...args: any[]) => void; | ||
| type ActionMethod = (...args: any[]) => Promise<any>; | ||
| type CustomVue = Vue & Dictionary<any> | ||
|
|
||
| interface Mapper<R> { | ||
| (map: string[]): Dictionary<R>; | ||
| (map: Dictionary<string>): Dictionary<R>; | ||
| /** | ||
| * Utility types to declare helper types | ||
| */ | ||
| type Computed<R> = () => R; | ||
| type Method<R> = (...args: any[]) => R; | ||
| type CustomVue = Vue & Record<string, any>; | ||
|
|
||
| interface BaseType { [key: string]: any } | ||
|
|
||
| interface BaseStateMap<State, Getters> { | ||
| [key: string]: (this: CustomVue, state: State, getters: Getters) => any; | ||
| } | ||
|
|
||
| interface BaseMethodMap<F> { | ||
| [key: string]: (this: CustomVue, fn: F, ...args: any[]) => any; | ||
| } | ||
|
|
||
| type MethodType = 'optional' | 'normal' | ||
|
|
||
| /** | ||
| * Return component method type for a mutation. | ||
| * You can specify `Type` to choose whether the argument is optional or not. | ||
| */ | ||
| type MutationMethod<P, Type extends MethodType> = { | ||
| optional: (payload?: P) => void; | ||
| normal: (payload: P) => void; | ||
| }[Type]; | ||
|
|
||
| /** | ||
| * Return component method type for an action. | ||
| * You can specify `Type` to choose whether the argument is optional or not. | ||
| */ | ||
| type ActionMethod<P, Type extends MethodType> = { | ||
| optional: (payload?: P) => Promise<any>; | ||
| normal: (payload: P) => Promise<any>; | ||
| }[Type]; | ||
|
|
||
| /** | ||
| * mapGetters | ||
| */ | ||
| interface MapGetters<Getters> { | ||
| <Key extends keyof Getters>(map: Key[]): { [K in Key]: Computed<Getters[K]> }; | ||
| <Map extends Record<string, keyof Getters>>(map: Map): { [K in keyof Map]: Computed<Getters[Map[K]]> }; | ||
| } | ||
|
|
||
| interface RootMapGetters<Getters> extends MapGetters<Getters> { | ||
| <Key extends keyof Getters>(namespace: string, map: Key[]): { [K in Key]: Computed<Getters[K]> }; | ||
| <Map extends Record<string, keyof Getters>>(namespace: string, map: Map): { [K in keyof Map]: Computed<Getters[Map[K]]> }; | ||
| } | ||
|
|
||
| /** | ||
| * mapState | ||
| */ | ||
| interface MapState<State, Getters> { | ||
| <Key extends keyof State>(map: Key[]): { [K in Key]: Computed<State[K]> }; | ||
| <Map extends Record<string, keyof State>>(map: Map): { [K in keyof Map]: Computed<State[Map[K]]> }; | ||
| <Map extends BaseStateMap<State, Getters>>(map: Map): { [K in keyof Map]: Computed<any> }; | ||
| } | ||
|
|
||
| interface MapperWithNamespace<R> { | ||
| (namespace: string, map: string[]): Dictionary<R>; | ||
| (namespace: string, map: Dictionary<string>): Dictionary<R>; | ||
| interface RootMapState<State, Getters> extends MapState<State, Getters> { | ||
| <Key extends keyof State>(namespace: string, map: Key[]): { [K in Key]: Computed<State[K]> }; | ||
| <Map extends Record<string, keyof State>>(namespace: string, map: Map): { [K in keyof Map]: Computed<State[Map[K]]> }; | ||
| <Map extends BaseStateMap<State, Getters>>(namespace: string, map: Map): { [K in keyof Map]: Computed<any> }; | ||
| } | ||
|
|
||
| interface FunctionMapper<F, R> { | ||
| (map: Dictionary<(this: CustomVue, fn: F, ...args: any[]) => any>): Dictionary<R>; | ||
| /** | ||
| * mapMutations | ||
| */ | ||
| interface MapMutations<Mutations, Type extends MethodType> { | ||
| <Key extends keyof Mutations>(map: Key[]): { [K in Key]: MutationMethod<Mutations[K], Type> }; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| <Map extends Record<string, keyof Mutations>>(map: Map): { [K in keyof Map]: MutationMethod<Mutations[Map[K]], Type> }; | ||
| <Map extends BaseMethodMap<Commit<Mutations>>>(map: Map): { [K in keyof Map]: Method<any> }; | ||
| } | ||
|
|
||
| interface FunctionMapperWithNamespace<F, R> { | ||
| ( | ||
| namespace: string, | ||
| map: Dictionary<(this: CustomVue, fn: F, ...args: any[]) => any> | ||
| ): Dictionary<R>; | ||
| interface RootMapMutations<Mutations, Type extends MethodType> extends MapMutations<Mutations, Type> { | ||
| <Key extends keyof Mutations>(namespace: string, map: Key[]): { [K in Key]: MutationMethod<Mutations[K], Type> }; | ||
| <Map extends Record<string, keyof Mutations>>(namespace: string, map: Map): { [K in keyof Map]: MutationMethod<Mutations[Map[K]], Type> }; | ||
| <Map extends BaseMethodMap<Commit<Mutations>>>(namespace: string, map: Map): { [K in keyof Map]: Method<any> }; | ||
| } | ||
|
|
||
| interface MapperForState { | ||
| <S>( | ||
| map: Dictionary<(this: CustomVue, state: S, getters: any) => any> | ||
| ): Dictionary<Computed>; | ||
| /** | ||
| * mapActions | ||
| */ | ||
| interface MapActions<Actions, Type extends MethodType> { | ||
| <Key extends keyof Actions>(map: Key[]): { [K in Key]: ActionMethod<Actions[K], Type> }; | ||
| <Map extends Record<string, keyof Actions>>(map: Map): { [K in keyof Map]: ActionMethod<Actions[Map[K]], Type> }; | ||
| <Map extends BaseMethodMap<Dispatch<Actions>>>(map: Map): { [K in keyof Map]: Method<any> }; | ||
| } | ||
|
|
||
| interface MapperForStateWithNamespace { | ||
| <S>( | ||
| namespace: string, | ||
| map: Dictionary<(this: CustomVue, state: S, getters: any) => any> | ||
| ): Dictionary<Computed>; | ||
| interface RootMapActions<Actions, Type extends MethodType> extends MapActions<Actions, Type> { | ||
| <Key extends keyof Actions>(namespace: string, map: Key[]): { [K in Key]: ActionMethod<Actions[K], Type> }; | ||
| <Map extends Record<string, keyof Actions>>(namespace: string, map: Map): { [K in keyof Map]: ActionMethod<Actions[Map[K]], Type> }; | ||
| <Map extends BaseMethodMap<Dispatch<Actions>>>(namespace: string, map: Map): { [K in keyof Map]: Method<any> }; | ||
| } | ||
|
|
||
| interface NamespacedMappers { | ||
| mapState: Mapper<Computed> & MapperForState; | ||
| mapMutations: Mapper<MutationMethod> & FunctionMapper<Commit, MutationMethod>; | ||
| mapGetters: Mapper<Computed>; | ||
| mapActions: Mapper<ActionMethod> & FunctionMapper<Dispatch, ActionMethod>; | ||
| /** | ||
| * namespaced helpers | ||
| */ | ||
| interface NamespacedMappers<State, Getters, Mutations, Actions, Type extends MethodType> { | ||
| mapState: MapState<State, Getters>; | ||
| mapGetters: MapGetters<Getters>; | ||
| mapMutations: MapMutations<Mutations, Type>; | ||
| mapActions: MapActions<Actions, Type>; | ||
| } | ||
|
|
||
| export declare const mapState: Mapper<Computed> | ||
| & MapperWithNamespace<Computed> | ||
| & MapperForState | ||
| & MapperForStateWithNamespace; | ||
| export declare const mapState: RootMapState<BaseType, BaseType>; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The proposed usage is Another usage is that we export export interface RootMapState<State> {
<Keys extends keyof State>(keys: Keys[]): {[K in Keys]: State[K]}
}then users can write something like But it is more versatile in usage -- I think object style can be supported, and more precise in returning type.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about reusing the pattern of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That sounds good idea. I think returning root helpers from
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome! Great balance between types and runtime behavior. |
||
|
|
||
| export declare const mapMutations: Mapper<MutationMethod> | ||
| & MapperWithNamespace<MutationMethod> | ||
| & FunctionMapper<Commit, MutationMethod> | ||
| & FunctionMapperWithNamespace<Commit, MutationMethod>; | ||
| export declare const mapMutations: RootMapMutations<BaseType, 'optional'>; | ||
|
|
||
| export declare const mapGetters: Mapper<Computed> | ||
| & MapperWithNamespace<Computed>; | ||
| export declare const mapGetters: RootMapGetters<BaseType>; | ||
|
|
||
| export declare const mapActions: Mapper<ActionMethod> | ||
| & MapperWithNamespace<ActionMethod> | ||
| & FunctionMapper<Dispatch, ActionMethod> | ||
| & FunctionMapperWithNamespace<Dispatch, ActionMethod>; | ||
| export declare const mapActions: RootMapActions<BaseType, 'optional'>; | ||
|
|
||
| export declare function createNamespacedHelpers(namespace: string): NamespacedMappers; | ||
| export declare function createNamespacedHelpers(namespace?: string): NamespacedMappers<BaseType, BaseType, BaseType, BaseType, 'optional'>; | ||
| export declare function createNamespacedHelpers<State, Getters, Mutations, Actions>(namespace?: string): NamespacedMappers<State, Getters, Mutations, Actions, 'normal'>; | ||
Uh oh!
There was an error while loading. Please reload this page.
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
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!