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

feat: improve helper types for more type safety #1121

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0768c80
feat: improve helper types to utilize Vue 2.5 types
ktsn Nov 6, 2017
25775c5
feat(types): [WIP] more strict typed store assets
ktsn Jan 4, 2018
21ee399
Merge branch 'dev' into feat-improve-typing
ktsn Jan 4, 2018
eee1f3c
fix(types): relax map state function type
ktsn Jan 4, 2018
f94cf70
test(types): update namespaced helper type test
ktsn Jan 4, 2018
a5c4e26
feat(types): allow to specify assets types on mapXXX helpers
ktsn Jan 4, 2018
58d28a5
chore(types): add comments for helpers and utilities types
ktsn Jan 4, 2018
0858c6d
fix(types): revert renaming Payload to avoid breaking change
ktsn Jan 9, 2018
1e27c5e
feat(helpers): return root helpers if no namespace is provided to cre…
ktsn Jan 9, 2018
c2068f3
feat(types): add `DefineModule` utility type
ktsn Jan 9, 2018
7abf34f
fix(types): allow to omit payload on mapped methods if it is untyped
ktsn Jan 9, 2018
9564b80
docs(helpers): improve `createNamespacedHelpers` description
ktsn Jan 11, 2018
1aa407f
fix(types): expose DefineGetters/Mutations/Actions type
ktsn Jan 11, 2018
cfb6042
chore: include utils.d.ts for `files` field
ktsn Jan 11, 2018
00360b5
fix(types): make dispatch/commit more type safe in module actions if …
ktsn Jan 17, 2018
9b89ae7
fix(types): remove default type parameters from StrictDispatch/Commit
ktsn Jan 17, 2018
09475d5
refactor: use undefined type to indicate empty payload instead of null
ktsn Jan 17, 2018
8e0c60b
fix(types): fix incorrect type annotation
ktsn Jan 17, 2018
b14662c
fix(types): fix ActionContext type
ktsn Jan 17, 2018
b24d744
fix(types): remove incorrect overload
ktsn Jan 18, 2018
8b6a6f9
refactor(types): just use Dispatch/CommitOptions instead of BaseDispa…
ktsn Jan 18, 2018
9b183f0
docs: add typescript docs
ktsn Jan 22, 2018
715eaad
docs: fix typo in typescript docs
ktsn Jan 22, 2018
fc1f29b
docs: add a note about conditional types
ktsn Jan 25, 2018
c3626f7
Merge branch 'dev' into feat-improve-typing
ktsn Feb 1, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/en/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ const store = new Vuex.Store({ ...options })

The first argument can optionally be a namespace string. [Details](modules.md#binding-helpers-with-namespace)

- **`createNamespacedHelpers(namespace: string): Object`**
- **`createNamespacedHelpers(namespace?: string): Object`**

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

@HerringtonDarkholme HerringtonDarkholme Jan 10, 2018

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.

Copy link
Member Author

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!

12 changes: 7 additions & 5 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,16 @@ export const mapActions = normalizeNamespace((namespace, actions) => {

/**
* Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
* @param {String} namespace
* If the namespace is not specified, it returns the root mapXXX helpers.
* This behavior is convenient to annotate strict types for mapXXX helpers.
* @param {String} [namespace]
* @return {Object}
*/
export const createNamespacedHelpers = (namespace) => ({
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
mapState: namespace ? mapState.bind(null, namespace) : mapState,
mapGetters: namespace ? mapGetters.bind(null, namespace) : mapGetters,
mapMutations: namespace ? mapMutations.bind(null, namespace) : mapMutations,
mapActions: namespace ? mapActions.bind(null, namespace) : mapActions
})

/**
Expand Down
52 changes: 52 additions & 0 deletions test/unit/helpers.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,4 +517,56 @@ describe('Helpers', () => {
vm.actionB()
expect(actionB).toHaveBeenCalled()
})

it('createNamespacedHelpers: generates root helpers', () => {
const actionA = jasmine.createSpy()
const actionB = jasmine.createSpy()
const store = new Vuex.Store({
state: { count: 0 },
getters: {
isEven: state => state.count % 2 === 0
},
mutations: {
inc: state => state.count++,
dec: state => state.count--
},
actions: {
actionA,
actionB
}
})
const {
mapState,
mapGetters,
mapMutations,
mapActions
} = createNamespacedHelpers()
const vm = new Vue({
store,
computed: {
...mapState(['count']),
...mapGetters(['isEven'])
},
methods: {
...mapMutations(['inc', 'dec']),
...mapActions(['actionA', 'actionB'])
}
})
expect(vm.count).toBe(0)
expect(vm.isEven).toBe(true)
store.state.count++
expect(vm.count).toBe(1)
expect(vm.isEven).toBe(false)
vm.inc()
expect(store.state.count).toBe(2)
expect(store.getters.isEven).toBe(true)
vm.dec()
expect(store.state.count).toBe(1)
expect(store.getters.isEven).toBe(false)
vm.actionA()
expect(actionA).toHaveBeenCalled()
expect(actionB).not.toHaveBeenCalled()
vm.actionB()
expect(actionB).toHaveBeenCalled()
})
})
48 changes: 24 additions & 24 deletions types/helpers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,58 +24,58 @@ interface BaseMethodMap<F> {
* mapGetters
*/
interface MapGetters<Getters> {
<G extends Getters = Getters, Key extends keyof G = keyof G>(map: Key[]): { [K in Key]: Computed<G[K]> };
<G extends Getters = Getters, Map extends Record<string, keyof G> = Record<string, keyof G>>(map: Map): { [K in keyof Map]: Computed<G[Map[K]]> };
<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> {
<G extends Getters = Getters, Key extends keyof G = keyof G>(namespace: string, map: Key[]): { [K in Key]: Computed<G[K]> };
<G extends Getters = Getters, Map extends Record<string, keyof G> = Record<string, keyof G>>(namespace: string, map: Map): { [K in keyof Map]: Computed<G[Map[K]]> };
<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> {
<S extends State = State, Key extends keyof S = keyof S>(map: Key[]): { [K in Key]: Computed<S[K]> };
<S extends State = State, Map extends Record<string, keyof S> = Record<string, keyof S>>(map: Map): { [K in keyof Map]: Computed<S[Map[K]]> };
<S extends State = State, G extends Getters = Getters, Map extends BaseStateMap<S, G> = BaseStateMap<S, G>>(map: Map): { [K in keyof Map]: Computed<any> };
<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 RootMapState<State, Getters> extends MapState<State, Getters> {
<S extends State = State, Key extends keyof S = keyof S>(namespace: string, map: Key[]): { [K in Key]: Computed<S[K]> };
<S extends State = State, Map extends Record<string, keyof S> = Record<string, keyof S>>(namespace: string, map: Map): { [K in keyof Map]: Computed<S[Map[K]]> };
<S extends State = State, G extends Getters = Getters, Map extends BaseStateMap<S, G> = BaseStateMap<S, G>>(namespace: string, map: Map): { [K in keyof Map]: Computed<any> };
<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> };
}

/**
* mapMutations
*/
interface MapMutations<Mutations> {
<M extends Mutations = Mutations, Key extends keyof M = keyof M>(map: Key[]): { [K in Key]: MutationMethod<M[K]> };
<M extends Mutations = Mutations, Map extends Record<string, keyof M> = Record<string, keyof M>>(map: Map): { [K in keyof Map]: MutationMethod<M[Map[K]]> };
<M extends Mutations = Mutations, Map extends BaseMethodMap<Commit<M>> = BaseMethodMap<Commit<M>>>(map: Map): { [K in keyof Map]: Method<any> };
<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]]> };
<Map extends BaseMethodMap<Commit<Mutations>>>(map: Map): { [K in keyof Map]: Method<any> };
}

interface RootMapMutations<Mutations> extends MapMutations<Mutations> {
<M extends Mutations = Mutations, Key extends keyof M = keyof M>(namespace: string, map: Key[]): { [K in Key]: MutationMethod<M[K]> };
<M extends Mutations = Mutations, Map extends Record<string, keyof M> = Record<string, keyof M>>(namespace: string, map: Map): { [K in keyof Map]: MutationMethod<M[Map[K]]> };
<M extends Mutations = Mutations, Map extends BaseMethodMap<Commit<M>> = BaseMethodMap<Commit<M>>>(namespace: string, map: Map): { [K in keyof Map]: Method<any> };
<Key extends keyof Mutations>(namespace: string, map: Key[]): { [K in Key]: MutationMethod<Mutations[K]> };
<Map extends Record<string, keyof Mutations>>(namespace: string, map: Map): { [K in keyof Map]: MutationMethod<Mutations[Map[K]]> };
<Map extends BaseMethodMap<Commit<Mutations>>>(namespace: string, map: Map): { [K in keyof Map]: Method<any> };
}

/**
* mapActions
*/
interface MapActions<Actions> {
<A extends Actions = Actions, Key extends keyof A = keyof A>(map: Key[]): { [K in Key]: ActionMethod<A[K]> };
<A extends Actions = Actions, Map extends Record<string, keyof A> = Record<string, keyof A>>(map: Map): { [K in keyof Map]: ActionMethod<A[Map[K]]> };
<A extends Actions = Actions, Map extends BaseMethodMap<Dispatch<A>> = BaseMethodMap<Dispatch<A>>>(map: Map): { [K in keyof Map]: Method<any> };
<Key extends keyof Actions>(map: Key[]): { [K in Key]: ActionMethod<Actions[K]> };
<Map extends Record<string, keyof Actions>>(map: Map): { [K in keyof Map]: ActionMethod<Actions[Map[K]]> };
<Map extends BaseMethodMap<Dispatch<Actions>>>(map: Map): { [K in keyof Map]: Method<any> };
}

interface RootMapActions<Actions> extends MapActions<Actions> {
<A extends Actions = Actions, Key extends keyof A = keyof A>(namespace: string, map: Key[]): { [K in Key]: ActionMethod<A[K]> };
<A extends Actions = Actions, Map extends Record<string, keyof A> = Record<string, keyof A>>(namespace: string, map: Map): { [K in keyof Map]: ActionMethod<A[Map[K]]> };
<A extends Actions = Actions, Map extends BaseMethodMap<Dispatch<A>> = BaseMethodMap<Dispatch<A>>>(namespace: string, map: Map): { [K in keyof Map]: Method<any> };
<Key extends keyof Actions>(namespace: string, map: Key[]): { [K in Key]: ActionMethod<Actions[K]> };
<Map extends Record<string, keyof Actions>>(namespace: string, map: Map): { [K in keyof Map]: ActionMethod<Actions[Map[K]]> };
<Map extends BaseMethodMap<Dispatch<Actions>>>(namespace: string, map: Map): { [K in keyof Map]: Method<any> };
}

/**
Expand All @@ -96,5 +96,5 @@ export declare const mapGetters: RootMapGetters<BaseType>;

export declare const mapActions: RootMapActions<BaseType>;

export declare function createNamespacedHelpers(namespace: string): NamespacedMappers<BaseType, BaseType, BaseType, BaseType>;
export declare function createNamespacedHelpers<State, Getters, Mutations, Actions>(namespace: string): NamespacedMappers<State, Getters, Mutations, Actions>;
export declare function createNamespacedHelpers(namespace?: string): NamespacedMappers<BaseType, BaseType, BaseType, BaseType>;
export declare function createNamespacedHelpers<State, Getters, Mutations, Actions>(namespace?: string): NamespacedMappers<State, Getters, Mutations, Actions>;
4 changes: 3 additions & 1 deletion types/test/shopping-cart/app.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import Vue from 'vue'
import { cartHelpers } from './store/modules/cart'
import store from './store'
import store, { rootHelpers } from './store'

new Vue({
store,

computed: {
...rootHelpers.mapState(['cart']),
...cartHelpers.mapState({
test: (state, getters) => {
state.added
Expand All @@ -24,6 +25,7 @@ new Vue({
},

created () {
this.cart
this.test
this.items
this.checkoutStatus
Expand Down
4 changes: 3 additions & 1 deletion types/test/shopping-cart/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Vue from 'vue'
import Vuex from '../../../index'
import Vuex, { createNamespacedHelpers } from '../../../index'
import cart, { CartState } from './modules/cart'
import products, { ProductsState } from './modules/products'

Expand All @@ -10,6 +10,8 @@ export interface RootState {
products: ProductsState
}

export const rootHelpers = createNamespacedHelpers<RootState, {}, {}, {}>()

export default new Vuex.Store({
modules: {
cart,
Expand Down