From 0768c802b0634ff44f3d8f922b718511398efb34 Mon Sep 17 00:00:00 2001 From: ktsn Date: Mon, 6 Nov 2017 12:10:56 +0900 Subject: [PATCH 01/23] feat: improve helper types to utilize Vue 2.5 types --- types/helpers.d.ts | 102 +++++++++++--------- types/test/helpers.ts | 218 ++++++++++++++++++++++++++---------------- 2 files changed, 193 insertions(+), 127 deletions(-) diff --git a/types/helpers.d.ts b/types/helpers.d.ts index e86650485..016ec7633 100644 --- a/types/helpers.d.ts +++ b/types/helpers.d.ts @@ -1,68 +1,76 @@ import Vue from 'vue'; import { Dispatch, Commit } from './index'; -type Dictionary = { [key: string]: T }; -type Computed = () => any; -type MutationMethod = (...args: any[]) => void; -type ActionMethod = (...args: any[]) => Promise; - -interface Mapper { - (map: string[]): Dictionary; - (map: Dictionary): Dictionary; +type Computed = () => R; +type MutationMethod

= (payload: P) => void; +type ActionMethod

= (payload: P) => Promise; + +type StateAccessor = { + [K in keyof T]: (this: Vue, state: State, getters: Getters) => T[K]; +} & { + [key: string]: (this: Vue, state: State, getters: Getters) => any; +} + +interface BaseType { [key: string]: any } + +type BaseMethodMap = { [key: string]: (this: Vue, fn: F, ...args: any[]) => any } + +interface MapGetters { + (map: Key[]): { [K in Key]: Computed }; + = Record>(map: Map): { [K in keyof Map]: Computed }; +} + +interface RootMapGetters extends MapGetters { + (namespace: string, map: Key[]): { [K in Key]: Computed }; + = Record>(namespace: string, map: Map): { [K in keyof Map]: Computed }; +} + +interface MapState extends MapGetters { + (map: StateAccessor): { [K in keyof T]: Computed }; } -interface MapperWithNamespace { - (namespace: string, map: string[]): Dictionary; - (namespace: string, map: Dictionary): Dictionary; +type CombinedMapState = MapState & RootMapGetters +interface RootMapState extends CombinedMapState { + (namespace: string, map: StateAccessor): { [K in keyof T]: Computed }; } -interface FunctionMapper { - (map: Dictionary<(this: typeof Vue, fn: F, ...args: any[]) => any>): Dictionary; +interface MapMutations { + (map: Key[]): { [K in Key]: MutationMethod }; + = Record>(map: Map): { [K in keyof Map]: MutationMethod }; + >(map: T): { [K in keyof T]: Function }; } -interface FunctionMapperWithNamespace { - ( - namespace: string, - map: Dictionary<(this: typeof Vue, fn: F, ...args: any[]) => any> - ): Dictionary; +interface RootMapMutations extends MapMutations { + (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; + = Record>(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; + >(namespace: string, map: T): { [K in keyof T]: Function }; } -interface MapperForState { - ( - map: Dictionary<(this: typeof Vue, state: S, getters: any) => any> - ): Dictionary; +interface MapActions { + (map: Key[]): { [K in Key]: ActionMethod }; + = Record>(map: Map): { [K in keyof Map]: ActionMethod }; + >(map: T): { [K in keyof T]: Function }; } -interface MapperForStateWithNamespace { - ( - namespace: string, - map: Dictionary<(this: typeof Vue, state: S, getters: any) => any> - ): Dictionary; +interface RootMapActions extends MapActions { + (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; + = Record>(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; + >(namespace: string, map: T): { [K in keyof T]: Function }; } -interface NamespacedMappers { - mapState: Mapper & MapperForState; - mapMutations: Mapper & FunctionMapper; - mapGetters: Mapper; - mapActions: Mapper & FunctionMapper; +interface NamespacedMappers { + mapState: MapState; + mapMutations: MapMutations; + mapGetters: MapGetters; + mapActions: MapActions; } -export declare const mapState: Mapper - & MapperWithNamespace - & MapperForState - & MapperForStateWithNamespace; +export declare const mapState: RootMapState; -export declare const mapMutations: Mapper - & MapperWithNamespace - & FunctionMapper - & FunctionMapperWithNamespace; +export declare const mapMutations: RootMapMutations; -export declare const mapGetters: Mapper - & MapperWithNamespace; +export declare const mapGetters: RootMapGetters; -export declare const mapActions: Mapper - & MapperWithNamespace - & FunctionMapper - & FunctionMapperWithNamespace; +export declare const mapActions: RootMapActions; -export declare function createNamespacedHelpers(namespace: string): NamespacedMappers; +export declare function createNamespacedHelpers(namespace: string): NamespacedMappers; diff --git a/types/test/helpers.ts b/types/test/helpers.ts index 460b692a4..4b024471c 100644 --- a/types/test/helpers.ts +++ b/types/test/helpers.ts @@ -11,76 +11,107 @@ import { const helpers = createNamespacedHelpers('foo'); new Vue({ - computed: Object.assign({}, - mapState(["a"]), - mapState('foo', ["a"]), - mapState({ - b: "b" - }), - mapState('foo', { - b: "b" - }), - mapState({ - c: (state: any, getters: any) => state.c + getters.c + computed: { + ...mapState(["a"]), + ...mapState('foo', ["b"]), + ...mapState({ + c: "c" + }), + ...mapState('foo', { + d: "d" + }), + ...mapState({ + e: (state: any, getters: any) => { + return state.a + getters.a + } }), - mapState('foo', { - c: (state: any, getters: any) => state.c + getters.c + ...mapState('foo', { + f: (state: any, getters: any) => { + return state.c + getters.c + } }), - mapGetters(["d"]), - mapGetters('foo', ["d"]), - mapGetters({ - e: "e" + ...helpers.mapState(["g"]), + ...helpers.mapState({ + h: "h" }), - mapGetters('foo', { - e: "e" + ...helpers.mapState({ + i: (state: any, getters: any) => state.k + getters.k }), - helpers.mapState(["k"]), - helpers.mapState({ - k: "k" + }, + + created () { + this.a + this.b + this.c + this.d + this.e + this.f + this.g + this.h + this.i + } +}) + +new Vue({ + computed: { + ...mapGetters(["a"]), + ...mapGetters('foo', ["b"]), + ...mapGetters({ + c: "c" }), - helpers.mapState({ - k: (state: any, getters: any) => state.k + getters.k + ...mapGetters('foo', { + d: "d" }), - helpers.mapGetters(["l"]), - helpers.mapGetters({ - l: "l" + ...helpers.mapGetters(["e"]), + ...helpers.mapGetters({ + f: "f" }), - { - otherComputed () { - return "f"; - } + otherComputed () { + return "g"; } - ), + }, - methods: Object.assign({}, - mapActions(["g"]), - mapActions({ - h: "h" + created () { + this.a + this.b + this.c + this.d + this.e + this.f + this.otherComputed + } +}) + +new Vue({ + methods: { + ...mapActions(["a"]), + ...mapActions({ + b: "b" }), - mapActions({ - g (dispatch, a: string, b: number, c: boolean): void { - dispatch('g', { a, b, c }) + ...mapActions({ + c (dispatch, a: string, b: number, c: boolean): void { + dispatch('c', { a, b, c }) dispatch({ - type: 'g', + type: 'c', a, b, c }) } }), - mapActions('foo', ["g"]), - mapActions('foo', { - h: "h" + ...mapActions('foo', ["d"]), + ...mapActions('foo', { + e: "e" }), - mapActions('foo', { - g (dispatch, a: string, b: number, c: boolean): void { - dispatch('g', { a, b, c }) + ...mapActions('foo', { + f (dispatch, a: string, b: number, c: boolean): void { + dispatch('f', { a, b, c }) dispatch({ - type: 'g', + type: 'f', a, b, c @@ -88,30 +119,56 @@ new Vue({ } }), - mapMutations(["i"]), - mapMutations({ - j: "j" + ...helpers.mapActions(["g"]), + ...helpers.mapActions({ + h: "h" + }), + ...helpers.mapActions({ + i (dispatch, value: string) { + dispatch('i', value) + } + }) + }, + + created () { + this.a(1) + this.b(2) + this.c('a', 3, true) + this.d(4) + this.e(5) + this.f(6) + this.g(7) + this.h(8) + this.i(9) + } +}) + +new Vue({ + methods: { + ...mapMutations(["a"]), + ...mapMutations({ + b: "b" }), - mapMutations({ - i (commit, a: string, b: number, c: boolean): void { - commit('i', { a, b, c }) + ...mapMutations({ + c (commit, a: string, b: number, c: boolean): void { + commit('c', { a, b, c }) commit({ - type: 'i', + type: 'c', a, b, c }) } }), - mapMutations('foo', ["i"]), - mapMutations('foo', { - j: "j" + ...mapMutations('foo', ["d"]), + ...mapMutations('foo', { + e: "e" }), - mapMutations('foo', { - i (commit, a: string, b: number, c: boolean): void { - commit('i', { a, b, c }) + ...mapMutations('foo', { + f (commit, a: string, b: number, c: boolean): void { + commit('f', { a, b, c }) commit({ - type: 'i', + type: 'f', a, b, c @@ -119,28 +176,29 @@ new Vue({ } }), - helpers.mapActions(["m"]), - helpers.mapActions({ - m: "m" + ...helpers.mapMutations(["g"]), + ...helpers.mapMutations({ + h: "h" }), - helpers.mapActions({ - m (dispatch, value: string) { - dispatch('m', value) + ...helpers.mapMutations({ + i (commit, value: string) { + commit('i', value) } }), - helpers.mapMutations(["n"]), - helpers.mapMutations({ - n: "n" - }), - helpers.mapMutations({ - n (commit, value: string) { - commit('m', value) - } - }), + otherMethod () {} + }, - { - otherMethod () {} - } - ) + created () { + this.a(1) + this.b(2) + this.c('a', 3, true) + this.d(4) + this.e(5) + this.f(6) + this.g(7) + this.h(8) + this.i(9) + this.otherMethod() + } }); From 25775c5944bde8b0dd15a2ae089b02a540dcc794 Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 4 Jan 2018 21:01:45 +0900 Subject: [PATCH 02/23] feat(types): [WIP] more strict typed store assets --- dist/logger.d.ts | 6 +- package-lock.json | 18 +-- package.json | 6 +- types/helpers.d.ts | 53 ++++---- types/index.d.ts | 100 ++++++++++++--- types/test/shopping-cart/api/shop.ts | 4 + types/test/shopping-cart/app.ts | 23 ++++ types/test/shopping-cart/store/index.ts | 18 +++ .../test/shopping-cart/store/modules/cart.ts | 120 ++++++++++++++++++ .../shopping-cart/store/modules/products.ts | 66 ++++++++++ types/utils.d.ts | 37 ++++++ 11 files changed, 389 insertions(+), 62 deletions(-) create mode 100644 types/test/shopping-cart/api/shop.ts create mode 100644 types/test/shopping-cart/app.ts create mode 100644 types/test/shopping-cart/store/index.ts create mode 100644 types/test/shopping-cart/store/modules/cart.ts create mode 100644 types/test/shopping-cart/store/modules/products.ts create mode 100644 types/utils.d.ts diff --git a/dist/logger.d.ts b/dist/logger.d.ts index 9863e850a..ad5747bf2 100644 --- a/dist/logger.d.ts +++ b/dist/logger.d.ts @@ -3,13 +3,13 @@ * This file must be put alongside the JavaScript file of the logger. */ -import { Payload, Plugin } from "../types/index"; +import { BasePayload, Plugin } from "../types/index"; export interface LoggerOption { collapsed?: boolean; - filter?:

(mutation: P, stateBefore: S, stateAfter: S) => boolean; + filter?:

(mutation: P, stateBefore: S, stateAfter: S) => boolean; transformer?: (state: S) => any; - mutationTransformer?:

(mutation: P) => any; + mutationTransformer?:

(mutation: P) => any; } export default function createLogger(option: LoggerOption): Plugin; diff --git a/package-lock.json b/package-lock.json index 7dd1facfc..4ba20f6c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7776,9 +7776,9 @@ "dev": true }, "typescript": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", - "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", + "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=", "dev": true }, "uglify-js": { @@ -7988,9 +7988,9 @@ } }, "vue": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.5.0.tgz", - "integrity": "sha512-KngZQLLe/N2Bvl3qu0xgqQHemm9MNz9y73D7yJ5tVavOKyhSgCLARYzrXJzYtoeadUSrItzV36VrHywLGVUx7w==", + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.5.13.tgz", + "integrity": "sha512-3D+lY7HTkKbtswDM4BBHgqyq+qo8IAEE8lz8va1dz3LLmttjgo0FxairO4r1iN2OBqk8o1FyL4hvzzTFEdQSEw==", "dev": true }, "vue-hot-reload-api": { @@ -8085,9 +8085,9 @@ } }, "vue-template-compiler": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.5.0.tgz", - "integrity": "sha512-W4hDoXXpCwfilO1MRTDM4EHm1DC1mU1wS8WyvEo119cUtxdaPuq/dD0OJbSEIkeW8fdT07qGCSnLOfPlmrKRqw==", + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.5.13.tgz", + "integrity": "sha512-15HWSgIxrGUcV0v7QRen2Y3fQsbgxXwMvjT/5XKMO0ANmaCcNh7y2OeIDTAuSGeosjb9+E1Pn2PHZ61VQWEgBQ==", "dev": true, "requires": { "de-indent": "1.0.2", diff --git a/package.json b/package.json index 13b0b6603..5a4c3a5ac 100644 --- a/package.json +++ b/package.json @@ -65,11 +65,11 @@ "rollup-watch": "^4.3.1", "selenium-server": "^2.53.1", "todomvc-app-css": "^2.1.0", - "typescript": "^2.5.3", + "typescript": "^2.6.1", "uglify-js": "^3.1.2", - "vue": "^2.5.0", + "vue": "^2.5.13", "vue-loader": "^13.3.0", - "vue-template-compiler": "^2.5.0", + "vue-template-compiler": "^2.5.13", "webpack": "^3.7.1", "webpack-dev-middleware": "^1.10.0", "webpack-hot-middleware": "^2.19.1" diff --git a/types/helpers.d.ts b/types/helpers.d.ts index 016ec7633..38f6794b4 100644 --- a/types/helpers.d.ts +++ b/types/helpers.d.ts @@ -16,56 +16,56 @@ interface BaseType { [key: string]: any } type BaseMethodMap = { [key: string]: (this: Vue, fn: F, ...args: any[]) => any } interface MapGetters { - (map: Key[]): { [K in Key]: Computed }; - = Record>(map: Map): { [K in keyof Map]: Computed }; + (map: Key[]): { [K in Key]: Computed }; + >(map: Map): { [K in keyof Map]: Computed }; } interface RootMapGetters extends MapGetters { - (namespace: string, map: Key[]): { [K in Key]: Computed }; - = Record>(namespace: string, map: Map): { [K in keyof Map]: Computed }; + (namespace: string, map: Key[]): { [K in Key]: Computed }; + >(namespace: string, map: Map): { [K in keyof Map]: Computed }; } -interface MapState extends MapGetters { - (map: StateAccessor): { [K in keyof T]: Computed }; +interface MapState extends MapGetters { + (map: StateAccessor): { [K in keyof T]: Computed }; } -type CombinedMapState = MapState & RootMapGetters -interface RootMapState extends CombinedMapState { - (namespace: string, map: StateAccessor): { [K in keyof T]: Computed }; +type CombinedMapState = MapState & RootMapGetters +interface RootMapState extends CombinedMapState { + (namespace: string, map: StateAccessor): { [K in keyof T]: Computed }; } interface MapMutations { - (map: Key[]): { [K in Key]: MutationMethod }; - = Record>(map: Map): { [K in keyof Map]: MutationMethod }; - >(map: T): { [K in keyof T]: Function }; + (map: Key[]): { [K in Key]: MutationMethod }; + >(map: Map): { [K in keyof Map]: MutationMethod }; + >>(map: T): { [K in keyof T]: Function }; } interface RootMapMutations extends MapMutations { - (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; - = Record>(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; - >(namespace: string, map: T): { [K in keyof T]: Function }; + (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; + >(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; + >>(namespace: string, map: T): { [K in keyof T]: Function }; } interface MapActions { - (map: Key[]): { [K in Key]: ActionMethod }; - = Record>(map: Map): { [K in keyof Map]: ActionMethod }; - >(map: T): { [K in keyof T]: Function }; + (map: Key[]): { [K in Key]: ActionMethod }; + >(map: Map): { [K in keyof Map]: ActionMethod }; + >>(map: T): { [K in keyof T]: Function }; } interface RootMapActions extends MapActions { - (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; - = Record>(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; - >(namespace: string, map: T): { [K in keyof T]: Function }; + (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; + >(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; + >>(namespace: string, map: T): { [K in keyof T]: Function }; } interface NamespacedMappers { - mapState: MapState; - mapMutations: MapMutations; - mapGetters: MapGetters; + mapState: MapState; + mapGetters: MapGetters; + mapMutations: MapMutations; mapActions: MapActions; } -export declare const mapState: RootMapState; +export declare const mapState: RootMapState; export declare const mapMutations: RootMapMutations; @@ -73,4 +73,5 @@ export declare const mapGetters: RootMapGetters; export declare const mapActions: RootMapActions; -export declare function createNamespacedHelpers(namespace: string): NamespacedMappers; +export declare function createNamespacedHelpers(namespace: string): NamespacedMappers; +export declare function createNamespacedHelpers(namespace: string): NamespacedMappers; diff --git a/types/index.d.ts b/types/index.d.ts index a504790a4..66b93a397 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3,7 +3,19 @@ import _Vue, { WatchOptions } from "vue"; // augment typings of Vue.js import "./vue"; -export * from "./helpers"; +export { + mapState, + mapGetters, + mapActions, + mapMutations, + createNamespacedHelpers +} from "./helpers"; + +export { + DefineGetters, + DefineMutations, + DefineActions +} from './utils' export declare class Store { constructor(options: StoreOptions); @@ -35,39 +47,85 @@ export declare class Store { export declare function install(Vue: typeof _Vue): void; -export interface Dispatch { - (type: string, payload?: any, options?: DispatchOptions): Promise; -

(payloadWithType: P, options?: DispatchOptions): Promise; +export interface Dispatch, RootActions = Record> { + // Local + (type: K, payload?: Actions[K], options?: LocalDispatchOptions): Promise; + (payloadWithType: Payload, options?: LocalDispatchOptions): Promise; + + // Root + (type: K, options: RootDispatchOptions): Promise; + (type: K, payload: RootActions[K], options: RootDispatchOptions): Promise; + (payloadWithType: Payload, options: RootDispatchOptions): Promise; +} + +export interface Commit, RootMutations = Record> { + // Local + (type: K, payload?: Mutations[K], options?: LocalCommitOptions): void; + (payloadWithType: Payload, options?: LocalCommitOptions): void; + + // Root + (type: K, options: RootCommitOptions): void; + (type: K, payload: RootMutations[K], options: RootCommitOptions): void; + (payloadWithType: Payload, options: RootCommitOptions): void; +} + +export interface ActionContext< + S, + RS, + G = any, + RG = any, + M = Record, + RM = Record, + A = Record, + RA = Record +> { + dispatch: Dispatch; + commit: Commit; + state: S; + getters: G; + rootState: RS; + rootGetters: RG; } -export interface Commit { - (type: string, payload?: any, options?: CommitOptions): void; -

(payloadWithType: P, options?: CommitOptions): void; +export interface BasePayload { + type: string; } -export interface ActionContext { - dispatch: Dispatch; - commit: Commit; - state: S; - getters: any; - rootState: R; - rootGetters: any; +type Payload = { type: K } & P[K] + +export interface MutationPayload extends BasePayload { + payload: any; } -export interface Payload { - type: string; +interface BaseDispatchOptions {} + +interface LocalDispatchOptions extends BaseDispatchOptions { + root?: false } -export interface MutationPayload extends Payload { - payload: any; +interface RootDispatchOptions extends BaseDispatchOptions { + root: true +} + +interface BaseCommitOptions { + silent?: boolean +} + +interface LocalCommitOptions extends BaseCommitOptions { + root?: false +} + +interface RootCommitOptions extends BaseCommitOptions { + root: true } -export interface DispatchOptions { +// Leave for backward compatibility +export interface DispatchOptions extends BaseDispatchOptions { root?: boolean; } -export interface CommitOptions { - silent?: boolean; +// Leave for backward compatibility +export interface CommitOptions extends BaseCommitOptions { root?: boolean; } diff --git a/types/test/shopping-cart/api/shop.ts b/types/test/shopping-cart/api/shop.ts new file mode 100644 index 000000000..41e17da1a --- /dev/null +++ b/types/test/shopping-cart/api/shop.ts @@ -0,0 +1,4 @@ +import { Product } from '../store/modules/products' + +export declare function buyProducts(products: Product[], cb: () => void, errorCb: () => void): void +export declare function getProducts(cb: (products: Product[]) => void): void \ No newline at end of file diff --git a/types/test/shopping-cart/app.ts b/types/test/shopping-cart/app.ts new file mode 100644 index 000000000..e664bce72 --- /dev/null +++ b/types/test/shopping-cart/app.ts @@ -0,0 +1,23 @@ +import Vue from 'vue' +import { cartHelpers } from './store/modules/cart' +import store from './store' + +new Vue({ + store, + + computed: { + ...cartHelpers.mapState({ + added: state => state.cart.added + }), + ...cartHelpers.mapGetters(['checkoutStatus']) + }, + + methods: { + ...cartHelpers.mapMutations(['addToCart']), + ...cartHelpers.mapActions(['checkout']) + }, + + created () { + + } +}) diff --git a/types/test/shopping-cart/store/index.ts b/types/test/shopping-cart/store/index.ts new file mode 100644 index 000000000..e7b1c5a9a --- /dev/null +++ b/types/test/shopping-cart/store/index.ts @@ -0,0 +1,18 @@ +import Vue from 'vue' +import Vuex from '../../../index' +import cart, { CartState } from './modules/cart' +import products, { ProductsState } from './modules/products' + +Vue.use(Vuex) + +export interface RootState { + cart: CartState + products: ProductsState +} + +export default new Vuex.Store({ + modules: { + cart, + products + } +}) diff --git a/types/test/shopping-cart/store/modules/cart.ts b/types/test/shopping-cart/store/modules/cart.ts new file mode 100644 index 000000000..ef3927fec --- /dev/null +++ b/types/test/shopping-cart/store/modules/cart.ts @@ -0,0 +1,120 @@ +import { createNamespacedHelpers, DefineGetters, DefineMutations, DefineActions } from '../../../../index' +import * as shop from '../../api/shop' +import { Product } from './products' +import { RootState } from '../' + +export interface AddedItem { + id: number + quantity: number +} + +export type CheckoutStatus = 'successful' | 'failed' | null + +export interface CartState { + added: AddedItem[] + checkoutStatus: CheckoutStatus +} + +export interface CartGetters { + checkoutStatus: CheckoutStatus + cartProducts: { + title: string + price: number + quantity: number + }[] +} + +export interface CartMutations { + addToCart: { + id: number + }, + checkoutRequest: null, + checkoutSuccess: null, + checkoutFailure: { + savedCartItems: AddedItem[] + } +} + +export interface CartActions { + checkout: Product[] + addToCart: Product +} + +const state: CartState = { + added: [], + checkoutStatus: null +} + +const getters: DefineGetters = { + checkoutStatus: state => state.checkoutStatus, + + cartProducts (state, getters, rootState, g) { + return state.added.map(({ id, quantity }) => { + const product = rootState.products.all.find(p => p.id === id) + return { + title: product.title, + price: product.price, + quantity + } + }) + } +} + +const actions: DefineActions = { + checkout ({ commit, state }, products) { + const savedCartItems = [...state.added] + commit('checkoutRequest') + shop.buyProducts( + products, + () => commit('checkoutSuccess'), + () => commit('checkoutFailure', { savedCartItems }) + ) + }, + + addToCart ({ commit }, product) { + if (product.inventory > 0) { + commit('addToCart', { + id: product.id + }) + } + } +} + +const mutations: DefineMutations = { + addToCart (state, { id }) { + state.checkoutStatus = null + const record = state.added.find(p => p.id === id) + if (!record) { + state.added.push({ + id, + quantity: 1 + }) + } else { + record.quantity++ + } + }, + + checkoutRequest (state) { + state.added = [] + state.checkoutStatus = null + }, + + checkoutSuccess (state) { + state.checkoutStatus = 'successful' + }, + + checkoutFailure (state, { savedCartItems }) { + state.added = savedCartItems + state.checkoutStatus = 'failed' + } +} + +export const cartHelpers = createNamespacedHelpers('cart') + +export default { + namespaced: true, + state, + getters, + actions, + mutations +} diff --git a/types/test/shopping-cart/store/modules/products.ts b/types/test/shopping-cart/store/modules/products.ts new file mode 100644 index 000000000..0e7c40e38 --- /dev/null +++ b/types/test/shopping-cart/store/modules/products.ts @@ -0,0 +1,66 @@ +import { createNamespacedHelpers, DefineGetters, DefineMutations, DefineActions } from '../../../../index' +import * as shop from '../../api/shop' + +export interface Product { + id: number + title: string + price: number + inventory: number +} + +export interface ProductsState { + all: Product[] +} + +export interface ProductsGetters { + allProducts: Product[] +} + +export interface ProductsActions { + getAllProducts: null +} + +export interface ProductsMutations { + receiveProducts: { + products: Product[] + }, + addToCart: { + id: number + } +} + +const state: ProductsState = { + all: [] +} + +const getters: DefineGetters = { + allProducts: state => state.all +} + +const actions: DefineActions = { + getAllProducts ({ commit }) { + shop.getProducts(products => { + commit('receiveProducts', { products }) + }) + } +} + +const mutations: DefineMutations = { + receiveProducts (state, { products }) { + state.all = products + }, + + addToCart (state, { id }) { + state.all.find(p => p.id === id).inventory-- + } +} + +export const productsHelpers = createNamespacedHelpers('products') + +export default { + namespaced: true, + state, + getters, + actions, + mutations +} diff --git a/types/utils.d.ts b/types/utils.d.ts new file mode 100644 index 000000000..c7e6a2dd9 --- /dev/null +++ b/types/utils.d.ts @@ -0,0 +1,37 @@ +import { ActionContext } from './index' + +export type DefineGetters< + Getters, + State, + ExtraGetters = {}, + RootState = {}, + RootGetters = {} +> = { + [K in keyof Getters]: ( + state: State, + getters: Getters & ExtraGetters, + rootState: RootState, + rootGetters: RootGetters + ) => Getters[K] +} + +export type DefineMutations = { + [K in keyof Mutations]: (state: State, payload: Mutations[K]) => void +} + +export type DefineActions< + Actions, + State, + Getters, + Mutations, + ExtraActions = {}, + RootState = {}, + RootGetters = {}, + RootMutations = {}, + RootActions = {} +> = { + [K in keyof Actions]: ( + ctx: ActionContext, + payload: Actions[K] + ) => Promise | void +} From eee1f3c7e7f77a7aa1c66bae6a2446b62efcc6a1 Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 4 Jan 2018 21:55:24 +0900 Subject: [PATCH 03/23] fix(types): relax map state function type --- types/helpers.d.ts | 32 ++++++++++++++++++-------------- types/test/helpers.ts | 8 ++++---- types/test/shopping-cart/app.ts | 2 +- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/types/helpers.d.ts b/types/helpers.d.ts index 8a442abff..ea096c745 100644 --- a/types/helpers.d.ts +++ b/types/helpers.d.ts @@ -2,19 +2,20 @@ import Vue from 'vue'; import { Dispatch, Commit } from './index'; type Computed = () => R; +type Method = (...args: any[]) => R; type MutationMethod

= (payload: P) => void; type ActionMethod

= (payload: P) => Promise; -type CustomVue = Vue & Record +type CustomVue = Vue & Record; -type StateAccessor = { - [K in keyof T]: (this: CustomVue, state: State, getters: Getters) => T[K]; -} & { +interface BaseStateMap { [key: string]: (this: CustomVue, state: State, getters: Getters) => any; } interface BaseType { [key: string]: any } -type BaseMethodMap = { [key: string]: (this: CustomVue, fn: F, ...args: any[]) => any } +interface BaseMethodMap { + [key: string]: (this: CustomVue, fn: F, ...args: any[]) => any; +} interface MapGetters { (map: Key[]): { [K in Key]: Computed }; @@ -26,37 +27,40 @@ interface RootMapGetters extends MapGetters { >(namespace: string, map: Map): { [K in keyof Map]: Computed }; } -interface MapState extends MapGetters { - (map: StateAccessor): { [K in keyof T]: Computed }; +interface MapState { + (map: Key[]): { [K in Key]: Computed }; + >(map: Map): { [K in keyof Map]: Computed }; + >(map: T): { [K in keyof T]: Computed }; } -type CombinedMapState = MapState & RootMapGetters -interface RootMapState extends CombinedMapState { - (namespace: string, map: StateAccessor): { [K in keyof T]: Computed }; +interface RootMapState extends MapState { + (namespace: string, map: Key[]): { [K in Key]: Computed }; + >(namespace: string, map: Map): { [K in keyof Map]: Computed }; + >(namespace: string, map: T): { [K in keyof T]: Computed }; } interface MapMutations { (map: Key[]): { [K in Key]: MutationMethod }; >(map: Map): { [K in keyof Map]: MutationMethod }; - >>(map: T): { [K in keyof T]: Function }; + >>(map: T): { [K in keyof T]: Method }; } interface RootMapMutations extends MapMutations { (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; >(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; - >>(namespace: string, map: T): { [K in keyof T]: Function }; + >>(namespace: string, map: T): { [K in keyof T]: Method }; } interface MapActions { (map: Key[]): { [K in Key]: ActionMethod }; >(map: Map): { [K in keyof Map]: ActionMethod }; - >>(map: T): { [K in keyof T]: Function }; + >>(map: T): { [K in keyof T]: Method }; } interface RootMapActions extends MapActions { (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; >(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; - >>(namespace: string, map: T): { [K in keyof T]: Function }; + >>(namespace: string, map: T): { [K in keyof T]: Method }; } interface NamespacedMappers { diff --git a/types/test/helpers.ts b/types/test/helpers.ts index a82664f3a..1ff3e025b 100644 --- a/types/test/helpers.ts +++ b/types/test/helpers.ts @@ -28,6 +28,9 @@ new Vue({ ...mapState('foo', { f: (state: any, getters: any) => { return state.c + getters.c + }, + useThis (state: any, getters: any) { + return state.c + getters.c + this.whatever } }), @@ -36,10 +39,7 @@ new Vue({ h: "h" }), ...helpers.mapState({ - i: (state: any, getters: any) => state.k + getters.k, - useThis (state: any, getters: any) { - return state.k + getters.k + this.whatever - } + i: (state: any, getters: any) => state.k + getters.k }), }, diff --git a/types/test/shopping-cart/app.ts b/types/test/shopping-cart/app.ts index e664bce72..b8c1ac419 100644 --- a/types/test/shopping-cart/app.ts +++ b/types/test/shopping-cart/app.ts @@ -7,7 +7,7 @@ new Vue({ computed: { ...cartHelpers.mapState({ - added: state => state.cart.added + added: state => state.added }), ...cartHelpers.mapGetters(['checkoutStatus']) }, From f94cf7026544970f644328093ef046853c290ad2 Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 4 Jan 2018 22:16:08 +0900 Subject: [PATCH 04/23] test(types): update namespaced helper type test --- types/test/helpers.ts | 3 +-- types/test/shopping-cart/app.ts | 19 +++++++++++++++++-- .../test/shopping-cart/store/modules/cart.ts | 2 +- .../shopping-cart/store/modules/products.ts | 2 +- types/test/tsconfig.json | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/types/test/helpers.ts b/types/test/helpers.ts index 1ff3e025b..cb22a9675 100644 --- a/types/test/helpers.ts +++ b/types/test/helpers.ts @@ -40,8 +40,7 @@ new Vue({ }), ...helpers.mapState({ i: (state: any, getters: any) => state.k + getters.k - }), - + }) }, created () { diff --git a/types/test/shopping-cart/app.ts b/types/test/shopping-cart/app.ts index b8c1ac419..e05173396 100644 --- a/types/test/shopping-cart/app.ts +++ b/types/test/shopping-cart/app.ts @@ -7,7 +7,13 @@ new Vue({ computed: { ...cartHelpers.mapState({ - added: state => state.added + test: (state, getters) => { + state.added + getters.cartProducts + } + }), + ...cartHelpers.mapState({ + items: 'added' }), ...cartHelpers.mapGetters(['checkoutStatus']) }, @@ -18,6 +24,15 @@ new Vue({ }, created () { - + this.test + this.items + this.checkoutStatus + this.addToCart({ id: 123 }) + this.checkout([{ + id: 123, + price: 3000, + title: 'test', + inventory: 3 + }]) } }) diff --git a/types/test/shopping-cart/store/modules/cart.ts b/types/test/shopping-cart/store/modules/cart.ts index ef3927fec..9c783a1dd 100644 --- a/types/test/shopping-cart/store/modules/cart.ts +++ b/types/test/shopping-cart/store/modules/cart.ts @@ -50,7 +50,7 @@ const getters: DefineGetters = { cartProducts (state, getters, rootState, g) { return state.added.map(({ id, quantity }) => { - const product = rootState.products.all.find(p => p.id === id) + const product = rootState.products.all.find(p => p.id === id)! return { title: product.title, price: product.price, diff --git a/types/test/shopping-cart/store/modules/products.ts b/types/test/shopping-cart/store/modules/products.ts index 0e7c40e38..ebf9e1ee0 100644 --- a/types/test/shopping-cart/store/modules/products.ts +++ b/types/test/shopping-cart/store/modules/products.ts @@ -51,7 +51,7 @@ const mutations: DefineMutations = { }, addToCart (state, { id }) { - state.all.find(p => p.id === id).inventory-- + state.all.find(p => p.id === id)!.inventory-- } } diff --git a/types/test/tsconfig.json b/types/test/tsconfig.json index 28f595aca..9f32dd27b 100644 --- a/types/test/tsconfig.json +++ b/types/test/tsconfig.json @@ -13,7 +13,7 @@ "noEmit": true }, "include": [ - "*.ts", + "**/*.ts", "../*.d.ts", "../../dist/logger.d.ts" ] From a5c4e2622c66f23f6e5e361bd22973df9e71f2ca Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 4 Jan 2018 22:44:53 +0900 Subject: [PATCH 05/23] feat(types): allow to specify assets types on mapXXX helpers --- types/helpers.d.ts | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/types/helpers.d.ts b/types/helpers.d.ts index ea096c745..bfa4ffb6e 100644 --- a/types/helpers.d.ts +++ b/types/helpers.d.ts @@ -18,49 +18,49 @@ interface BaseMethodMap { } interface MapGetters { - (map: Key[]): { [K in Key]: Computed }; - >(map: Map): { [K in keyof Map]: Computed }; + (map: Key[]): { [K in Key]: Computed }; + = Record>(map: Map): { [K in keyof Map]: Computed }; } interface RootMapGetters extends MapGetters { - (namespace: string, map: Key[]): { [K in Key]: Computed }; - >(namespace: string, map: Map): { [K in keyof Map]: Computed }; + (namespace: string, map: Key[]): { [K in Key]: Computed }; + = Record>(namespace: string, map: Map): { [K in keyof Map]: Computed }; } interface MapState { - (map: Key[]): { [K in Key]: Computed }; - >(map: Map): { [K in keyof Map]: Computed }; - >(map: T): { [K in keyof T]: Computed }; + (map: Key[]): { [K in Key]: Computed }; + = Record>(map: Map): { [K in keyof Map]: Computed }; + = BaseStateMap>(map: Map): { [K in keyof Map]: Computed }; } interface RootMapState extends MapState { - (namespace: string, map: Key[]): { [K in Key]: Computed }; - >(namespace: string, map: Map): { [K in keyof Map]: Computed }; - >(namespace: string, map: T): { [K in keyof T]: Computed }; + (namespace: string, map: Key[]): { [K in Key]: Computed }; + = Record>(namespace: string, map: Map): { [K in keyof Map]: Computed }; + = BaseStateMap>(namespace: string, map: Map): { [K in keyof Map]: Computed }; } interface MapMutations { - (map: Key[]): { [K in Key]: MutationMethod }; - >(map: Map): { [K in keyof Map]: MutationMethod }; - >>(map: T): { [K in keyof T]: Method }; + (map: Key[]): { [K in Key]: MutationMethod }; + = Record>(map: Map): { [K in keyof Map]: MutationMethod }; + > = BaseMethodMap>>(map: Map): { [K in keyof Map]: Method }; } interface RootMapMutations extends MapMutations { - (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; - >(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; - >>(namespace: string, map: T): { [K in keyof T]: Method }; + (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; + = Record>(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; + > = BaseMethodMap>>(namespace: string, map: Map): { [K in keyof Map]: Method }; } interface MapActions { - (map: Key[]): { [K in Key]: ActionMethod }; - >(map: Map): { [K in keyof Map]: ActionMethod }; - >>(map: T): { [K in keyof T]: Method }; + (map: Key[]): { [K in Key]: ActionMethod }; + = Record>(map: Map): { [K in keyof Map]: ActionMethod }; + > = BaseMethodMap>>(map: Map): { [K in keyof Map]: Method }; } interface RootMapActions extends MapActions { - (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; - >(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; - >>(namespace: string, map: T): { [K in keyof T]: Method }; + (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; + = Record>(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; + > = BaseMethodMap>>(namespace: string, map: Map): { [K in keyof Map]: Method }; } interface NamespacedMappers { From 58d28a54e0d84f96a5d7884376419574d1f5ab37 Mon Sep 17 00:00:00 2001 From: ktsn Date: Fri, 5 Jan 2018 00:20:58 +0900 Subject: [PATCH 06/23] chore(types): add comments for helpers and utilities types --- types/helpers.d.ts | 22 ++++++++++++++++++++-- types/utils.d.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/types/helpers.d.ts b/types/helpers.d.ts index bfa4ffb6e..cfbcc3d63 100644 --- a/types/helpers.d.ts +++ b/types/helpers.d.ts @@ -1,22 +1,28 @@ import Vue from 'vue'; import { Dispatch, Commit } from './index'; +/** + * Utility types to declare helper types + */ type Computed = () => R; type Method = (...args: any[]) => R; type MutationMethod

= (payload: P) => void; type ActionMethod

= (payload: P) => Promise; type CustomVue = Vue & Record; +interface BaseType { [key: string]: any } + interface BaseStateMap { [key: string]: (this: CustomVue, state: State, getters: Getters) => any; } -interface BaseType { [key: string]: any } - interface BaseMethodMap { [key: string]: (this: CustomVue, fn: F, ...args: any[]) => any; } +/** + * mapGetters + */ interface MapGetters { (map: Key[]): { [K in Key]: Computed }; = Record>(map: Map): { [K in keyof Map]: Computed }; @@ -27,6 +33,9 @@ interface RootMapGetters extends MapGetters { = Record>(namespace: string, map: Map): { [K in keyof Map]: Computed }; } +/** + * mapState + */ interface MapState { (map: Key[]): { [K in Key]: Computed }; = Record>(map: Map): { [K in keyof Map]: Computed }; @@ -39,6 +48,9 @@ interface RootMapState extends MapState { = BaseStateMap>(namespace: string, map: Map): { [K in keyof Map]: Computed }; } +/** + * mapMutations + */ interface MapMutations { (map: Key[]): { [K in Key]: MutationMethod }; = Record>(map: Map): { [K in keyof Map]: MutationMethod }; @@ -51,6 +63,9 @@ interface RootMapMutations extends MapMutations { > = BaseMethodMap>>(namespace: string, map: Map): { [K in keyof Map]: Method }; } +/** + * mapActions + */ interface MapActions { (map: Key[]): { [K in Key]: ActionMethod }; = Record>(map: Map): { [K in keyof Map]: ActionMethod }; @@ -63,6 +78,9 @@ interface RootMapActions extends MapActions { > = BaseMethodMap>>(namespace: string, map: Map): { [K in keyof Map]: Method }; } +/** + * namespaced helpers + */ interface NamespacedMappers { mapState: MapState; mapGetters: MapGetters; diff --git a/types/utils.d.ts b/types/utils.d.ts index c7e6a2dd9..b4e555f3c 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -1,5 +1,32 @@ import { ActionContext } from './index' +/* + * Type level utilities to annotate types of getters/actions/mutations (module assets). + * To use the below helpers, the user should declare corresponding assets type at first. + * + * A getters type should be an object that the keys indicate getter names + * and its corresponding values indicate return types of the getter. + * + * Actions type and mutations type should be an object that the keys indicate + * action/mutation names as same as the getters type. + * Its values should be declared as payload types of the actions/mutation. + * + * After declare the above types, the user put them on the generic parameters + * of the utility types. Then the real assets object must follow the passed types + * and type inference will work. + * + * The declared types will be used on mapXXX helpers to safely use module assets + * by annotating its types. + */ + +/** + * Infer getters object type from passed generic types. + * `Getters` is an object type that the keys indicate getter names and + * its corresponding values are return types of the getters. + * `State` is a module state type which is accessible in the getters. + * `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< Getters, State, @@ -15,10 +42,25 @@ export type DefineGetters< ) => Getters[K] } +/** + * Infer mutations object type from passed generic types. + * `Mutations` is an object type that the keys indicate mutation names and + * its corresponding values are payload types of the mutations. + * `State` is a module state type which will be mutated in the mutations. + */ export type DefineMutations = { [K in keyof Mutations]: (state: State, payload: Mutations[K]) => void } +/** + * Infer actions object type from passed generic types. + * `Actions` is an object type that the keys indicate action names and + * its corresponding values are payload types of the actions. + * `State`, `Getters`, `Mutations` are module state/getters/mutations type + * which can be accessed in actions. + * `ExtraActions` is like `Actions` type but will be not defined in the infered actions object. + * `RootState`, `RootGetters`, `RootMutations`, `RootActions` are the root module's asset types. + */ export type DefineActions< Actions, State, From 0858c6db067259130d5a74210f10eb5cec7bdb43 Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 9 Jan 2018 14:13:17 +0900 Subject: [PATCH 07/23] fix(types): revert renaming Payload to avoid breaking change --- dist/logger.d.ts | 6 +++--- types/index.d.ts | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dist/logger.d.ts b/dist/logger.d.ts index ad5747bf2..9863e850a 100644 --- a/dist/logger.d.ts +++ b/dist/logger.d.ts @@ -3,13 +3,13 @@ * This file must be put alongside the JavaScript file of the logger. */ -import { BasePayload, Plugin } from "../types/index"; +import { Payload, Plugin } from "../types/index"; export interface LoggerOption { collapsed?: boolean; - filter?:

(mutation: P, stateBefore: S, stateAfter: S) => boolean; + filter?:

(mutation: P, stateBefore: S, stateAfter: S) => boolean; transformer?: (state: S) => any; - mutationTransformer?:

(mutation: P) => any; + mutationTransformer?:

(mutation: P) => any; } export default function createLogger(option: LoggerOption): Plugin; diff --git a/types/index.d.ts b/types/index.d.ts index 8e7770b18..12c3392e7 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -51,23 +51,23 @@ export declare function install(Vue: typeof _Vue): void; export interface Dispatch, RootActions = Record> { // Local (type: K, payload?: Actions[K], options?: LocalDispatchOptions): Promise; - (payloadWithType: Payload, options?: LocalDispatchOptions): Promise; + (payloadWithType: InputPayload, options?: LocalDispatchOptions): Promise; // Root (type: K, options: RootDispatchOptions): Promise; (type: K, payload: RootActions[K], options: RootDispatchOptions): Promise; - (payloadWithType: Payload, options: RootDispatchOptions): Promise; + (payloadWithType: InputPayload, options: RootDispatchOptions): Promise; } export interface Commit, RootMutations = Record> { // Local (type: K, payload?: Mutations[K], options?: LocalCommitOptions): void; - (payloadWithType: Payload, options?: LocalCommitOptions): void; + (payloadWithType: InputPayload, options?: LocalCommitOptions): void; // Root (type: K, options: RootCommitOptions): void; (type: K, payload: RootMutations[K], options: RootCommitOptions): void; - (payloadWithType: Payload, options: RootCommitOptions): void; + (payloadWithType: InputPayload, options: RootCommitOptions): void; } export interface ActionContext< @@ -88,17 +88,17 @@ export interface ActionContext< rootGetters: RG; } -export interface BasePayload { +export interface Payload { type: string; } -type Payload = { type: K } & P[K] +type InputPayload = { type: K } & P[K] -export interface MutationPayload extends BasePayload { +export interface MutationPayload extends Payload { payload: any; } -export interface ActionPayload extends BasePayload { +export interface ActionPayload extends Payload { payload: any; } From 1e27c5e28e311837f3da2825cca18f0c5ee8e99b Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 9 Jan 2018 22:00:35 +0900 Subject: [PATCH 08/23] feat(helpers): return root helpers if no namespace is provided to createNamespacedHelpers --- docs/en/api.md | 4 +- src/helpers.js | 12 +++--- test/unit/helpers.spec.js | 52 +++++++++++++++++++++++++ types/helpers.d.ts | 48 +++++++++++------------ types/test/shopping-cart/app.ts | 4 +- types/test/shopping-cart/store/index.ts | 4 +- 6 files changed, 92 insertions(+), 32 deletions(-) diff --git a/docs/en/api.md b/docs/en/api.md index e117cc79f..df2d406fa 100644 --- a/docs/en/api.md +++ b/docs/en/api.md @@ -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. diff --git a/src/helpers.js b/src/helpers.js index c2f319afe..3f58d197f 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -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 }) /** diff --git a/test/unit/helpers.spec.js b/test/unit/helpers.spec.js index 1bc0fc409..118beb9cc 100644 --- a/test/unit/helpers.spec.js +++ b/test/unit/helpers.spec.js @@ -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() + }) }) diff --git a/types/helpers.d.ts b/types/helpers.d.ts index cfbcc3d63..7dbde4601 100644 --- a/types/helpers.d.ts +++ b/types/helpers.d.ts @@ -24,58 +24,58 @@ interface BaseMethodMap { * mapGetters */ interface MapGetters { - (map: Key[]): { [K in Key]: Computed }; - = Record>(map: Map): { [K in keyof Map]: Computed }; + (map: Key[]): { [K in Key]: Computed }; + >(map: Map): { [K in keyof Map]: Computed }; } interface RootMapGetters extends MapGetters { - (namespace: string, map: Key[]): { [K in Key]: Computed }; - = Record>(namespace: string, map: Map): { [K in keyof Map]: Computed }; + (namespace: string, map: Key[]): { [K in Key]: Computed }; + >(namespace: string, map: Map): { [K in keyof Map]: Computed }; } /** * mapState */ interface MapState { - (map: Key[]): { [K in Key]: Computed }; - = Record>(map: Map): { [K in keyof Map]: Computed }; - = BaseStateMap>(map: Map): { [K in keyof Map]: Computed }; + (map: Key[]): { [K in Key]: Computed }; + >(map: Map): { [K in keyof Map]: Computed }; + >(map: Map): { [K in keyof Map]: Computed }; } interface RootMapState extends MapState { - (namespace: string, map: Key[]): { [K in Key]: Computed }; - = Record>(namespace: string, map: Map): { [K in keyof Map]: Computed }; - = BaseStateMap>(namespace: string, map: Map): { [K in keyof Map]: Computed }; + (namespace: string, map: Key[]): { [K in Key]: Computed }; + >(namespace: string, map: Map): { [K in keyof Map]: Computed }; + >(namespace: string, map: Map): { [K in keyof Map]: Computed }; } /** * mapMutations */ interface MapMutations { - (map: Key[]): { [K in Key]: MutationMethod }; - = Record>(map: Map): { [K in keyof Map]: MutationMethod }; - > = BaseMethodMap>>(map: Map): { [K in keyof Map]: Method }; + (map: Key[]): { [K in Key]: MutationMethod }; + >(map: Map): { [K in keyof Map]: MutationMethod }; + >>(map: Map): { [K in keyof Map]: Method }; } interface RootMapMutations extends MapMutations { - (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; - = Record>(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; - > = BaseMethodMap>>(namespace: string, map: Map): { [K in keyof Map]: Method }; + (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; + >(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; + >>(namespace: string, map: Map): { [K in keyof Map]: Method }; } /** * mapActions */ interface MapActions { - (map: Key[]): { [K in Key]: ActionMethod }; - = Record>(map: Map): { [K in keyof Map]: ActionMethod }; - > = BaseMethodMap>>(map: Map): { [K in keyof Map]: Method }; + (map: Key[]): { [K in Key]: ActionMethod }; + >(map: Map): { [K in keyof Map]: ActionMethod }; + >>(map: Map): { [K in keyof Map]: Method }; } interface RootMapActions extends MapActions { - (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; - = Record>(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; - > = BaseMethodMap>>(namespace: string, map: Map): { [K in keyof Map]: Method }; + (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; + >(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; + >>(namespace: string, map: Map): { [K in keyof Map]: Method }; } /** @@ -96,5 +96,5 @@ export declare const mapGetters: RootMapGetters; export declare const mapActions: RootMapActions; -export declare function createNamespacedHelpers(namespace: string): NamespacedMappers; -export declare function createNamespacedHelpers(namespace: string): NamespacedMappers; +export declare function createNamespacedHelpers(namespace?: string): NamespacedMappers; +export declare function createNamespacedHelpers(namespace?: string): NamespacedMappers; diff --git a/types/test/shopping-cart/app.ts b/types/test/shopping-cart/app.ts index e05173396..0bb4a15d0 100644 --- a/types/test/shopping-cart/app.ts +++ b/types/test/shopping-cart/app.ts @@ -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 @@ -24,6 +25,7 @@ new Vue({ }, created () { + this.cart this.test this.items this.checkoutStatus diff --git a/types/test/shopping-cart/store/index.ts b/types/test/shopping-cart/store/index.ts index e7b1c5a9a..60b16f462 100644 --- a/types/test/shopping-cart/store/index.ts +++ b/types/test/shopping-cart/store/index.ts @@ -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' @@ -10,6 +10,8 @@ export interface RootState { products: ProductsState } +export const rootHelpers = createNamespacedHelpers() + export default new Vuex.Store({ modules: { cart, From c2068f32eb83f9ba1f8e2cbedd1d5bf3b1cb403f Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 9 Jan 2018 22:10:07 +0900 Subject: [PATCH 09/23] feat(types): add `DefineModule` utility type --- types/index.d.ts | 4 +- types/test/shopping-cart/store/index.ts | 4 +- .../test/shopping-cart/store/modules/cart.ts | 126 +++++++++--------- .../shopping-cart/store/modules/products.ts | 52 ++++---- types/utils.d.ts | 33 +++-- 5 files changed, 113 insertions(+), 106 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 12c3392e7..fe2e757a6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -12,9 +12,7 @@ export { } from "./helpers"; export { - DefineGetters, - DefineMutations, - DefineActions + DefineModule } from './utils' export declare class Store { diff --git a/types/test/shopping-cart/store/index.ts b/types/test/shopping-cart/store/index.ts index 60b16f462..d86973f58 100644 --- a/types/test/shopping-cart/store/index.ts +++ b/types/test/shopping-cart/store/index.ts @@ -1,7 +1,7 @@ import Vue from 'vue' import Vuex, { createNamespacedHelpers } from '../../../index' -import cart, { CartState } from './modules/cart' -import products, { ProductsState } from './modules/products' +import { cart, CartState } from './modules/cart' +import { products, ProductsState } from './modules/products' Vue.use(Vuex) diff --git a/types/test/shopping-cart/store/modules/cart.ts b/types/test/shopping-cart/store/modules/cart.ts index 9c783a1dd..ea2b417dd 100644 --- a/types/test/shopping-cart/store/modules/cart.ts +++ b/types/test/shopping-cart/store/modules/cart.ts @@ -1,4 +1,4 @@ -import { createNamespacedHelpers, DefineGetters, DefineMutations, DefineActions } from '../../../../index' +import { createNamespacedHelpers, DefineModule } from '../../../../index' import * as shop from '../../api/shop' import { Product } from './products' import { RootState } from '../' @@ -40,81 +40,77 @@ export interface CartActions { addToCart: Product } -const state: CartState = { - added: [], - checkoutStatus: null -} - -const getters: DefineGetters = { - checkoutStatus: state => state.checkoutStatus, +export const cartHelpers = createNamespacedHelpers('cart') - cartProducts (state, getters, rootState, g) { - return state.added.map(({ id, quantity }) => { - const product = rootState.products.all.find(p => p.id === id)! - return { - title: product.title, - price: product.price, - quantity - } - }) - } -} +export const cart: DefineModule = { + namespaced: true, -const actions: DefineActions = { - checkout ({ commit, state }, products) { - const savedCartItems = [...state.added] - commit('checkoutRequest') - shop.buyProducts( - products, - () => commit('checkoutSuccess'), - () => commit('checkoutFailure', { savedCartItems }) - ) + state: { + added: [], + checkoutStatus: null }, - addToCart ({ commit }, product) { - if (product.inventory > 0) { - commit('addToCart', { - id: product.id - }) - } - } -} - -const mutations: DefineMutations = { - addToCart (state, { id }) { - state.checkoutStatus = null - const record = state.added.find(p => p.id === id) - if (!record) { - state.added.push({ - id, - quantity: 1 + getters: { + checkoutStatus: state => state.checkoutStatus, + + cartProducts (state, getters, rootState, g) { + return state.added.map(({ id, quantity }) => { + const product = rootState.products.all.find(p => p.id === id)! + return { + title: product.title, + price: product.price, + quantity + } }) - } else { - record.quantity++ } }, - checkoutRequest (state) { - state.added = [] - state.checkoutStatus = null + actions: { + checkout ({ commit, state }, products) { + const savedCartItems = [...state.added] + commit('checkoutRequest') + shop.buyProducts( + products, + () => commit('checkoutSuccess'), + () => commit('checkoutFailure', { savedCartItems }) + ) + }, + + addToCart ({ commit }, product) { + if (product.inventory > 0) { + commit('addToCart', { + id: product.id + }) + } + } }, - checkoutSuccess (state) { - state.checkoutStatus = 'successful' - }, + mutations: { + addToCart (state, { id }) { + state.checkoutStatus = null + const record = state.added.find(p => p.id === id) + if (!record) { + state.added.push({ + id, + quantity: 1 + }) + } else { + record.quantity++ + } + }, - checkoutFailure (state, { savedCartItems }) { - state.added = savedCartItems - state.checkoutStatus = 'failed' - } -} + checkoutRequest (state) { + state.added = [] + state.checkoutStatus = null + }, -export const cartHelpers = createNamespacedHelpers('cart') + checkoutSuccess (state) { + state.checkoutStatus = 'successful' + }, -export default { - namespaced: true, - state, - getters, - actions, - mutations + checkoutFailure (state, { savedCartItems }) { + state.added = savedCartItems + state.checkoutStatus = 'failed' + } + } } diff --git a/types/test/shopping-cart/store/modules/products.ts b/types/test/shopping-cart/store/modules/products.ts index ebf9e1ee0..c55248438 100644 --- a/types/test/shopping-cart/store/modules/products.ts +++ b/types/test/shopping-cart/store/modules/products.ts @@ -1,4 +1,4 @@ -import { createNamespacedHelpers, DefineGetters, DefineMutations, DefineActions } from '../../../../index' +import { createNamespacedHelpers, DefineModule } from '../../../../index' import * as shop from '../../api/shop' export interface Product { @@ -29,38 +29,34 @@ export interface ProductsMutations { } } -const state: ProductsState = { - all: [] -} +export const productsHelpers = createNamespacedHelpers('products') -const getters: DefineGetters = { - allProducts: state => state.all -} +export const products: DefineModule = { + namespaced: true, -const actions: DefineActions = { - getAllProducts ({ commit }) { - shop.getProducts(products => { - commit('receiveProducts', { products }) - }) - } -} + state: { + all: [] + }, -const mutations: DefineMutations = { - receiveProducts (state, { products }) { - state.all = products + getters: { + allProducts: state => state.all }, - addToCart (state, { id }) { - state.all.find(p => p.id === id)!.inventory-- - } -} + actions: { + getAllProducts ({ commit }) { + shop.getProducts(products => { + commit('receiveProducts', { products }) + }) + } + }, -export const productsHelpers = createNamespacedHelpers('products') + mutations: { + receiveProducts (state, { products }) { + state.all = products + }, -export default { - namespaced: true, - state, - getters, - actions, - mutations + addToCart (state, { id }) { + state.all.find(p => p.id === id)!.inventory-- + } + } } diff --git a/types/utils.d.ts b/types/utils.d.ts index b4e555f3c..31d06d588 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -1,8 +1,8 @@ -import { ActionContext } from './index' +import { ActionContext, Module } from './index' -/* - * Type level utilities to annotate types of getters/actions/mutations (module assets). - * To use the below helpers, the user should declare corresponding assets type at first. +/** + * Type level utility to annotate types of module state/getters/actions/mutations (module assets). + * To use this helper, the user should declare corresponding assets type at first. * * A getters type should be an object that the keys indicate getter names * and its corresponding values indicate return types of the getter. @@ -12,12 +12,29 @@ import { ActionContext } from './index' * Its values should be declared as payload types of the actions/mutation. * * After declare the above types, the user put them on the generic parameters - * of the utility types. Then the real assets object must follow the passed types + * of the utility type. Then the real assets object must follow the passed types * and type inference will work. * * The declared types will be used on mapXXX helpers to safely use module assets * by annotating its types. */ +export interface DefineModule< + State, + Getters, + Mutations, + Actions, + ExtraGetters = {}, + ExtraMutations = {}, + ExtraActions = {}, + RootState = {}, + RootGetters = {}, + RootMutations = {}, + RootActions = {} +> extends Module { + getters?: DefineGetters + mutations?: DefineMutations + actions?: DefineActions +} /** * Infer getters object type from passed generic types. @@ -27,7 +44,7 @@ import { ActionContext } from './index' * `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< +type DefineGetters< Getters, State, ExtraGetters = {}, @@ -48,7 +65,7 @@ export type DefineGetters< * its corresponding values are payload types of the mutations. * `State` is a module state type which will be mutated in the mutations. */ -export type DefineMutations = { +type DefineMutations = { [K in keyof Mutations]: (state: State, payload: Mutations[K]) => void } @@ -61,7 +78,7 @@ export type DefineMutations = { * `ExtraActions` is like `Actions` type but will be not defined in the infered actions object. * `RootState`, `RootGetters`, `RootMutations`, `RootActions` are the root module's asset types. */ -export type DefineActions< +type DefineActions< Actions, State, Getters, From 7abf34f8ec10c372ca2899c44688137b47f0cf94 Mon Sep 17 00:00:00 2001 From: ktsn Date: Wed, 10 Jan 2018 01:40:12 +0900 Subject: [PATCH 10/23] fix(types): allow to omit payload on mapped methods if it is untyped --- types/helpers.d.ts | 60 ++++++++++++++++++++++++++++--------------- types/test/helpers.ts | 2 ++ 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/types/helpers.d.ts b/types/helpers.d.ts index 7dbde4601..d0c08911c 100644 --- a/types/helpers.d.ts +++ b/types/helpers.d.ts @@ -6,8 +6,6 @@ import { Dispatch, Commit } from './index'; */ type Computed = () => R; type Method = (...args: any[]) => R; -type MutationMethod

= (payload: P) => void; -type ActionMethod

= (payload: P) => Promise; type CustomVue = Vue & Record; interface BaseType { [key: string]: any } @@ -20,6 +18,26 @@ interface BaseMethodMap { [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 = { + 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 = { + optional: (payload?: P) => Promise; + normal: (payload: P) => Promise; +}[Type]; + /** * mapGetters */ @@ -51,50 +69,50 @@ interface RootMapState extends MapState { /** * mapMutations */ -interface MapMutations { - (map: Key[]): { [K in Key]: MutationMethod }; - >(map: Map): { [K in keyof Map]: MutationMethod }; +interface MapMutations { + (map: Key[]): { [K in Key]: MutationMethod }; + >(map: Map): { [K in keyof Map]: MutationMethod }; >>(map: Map): { [K in keyof Map]: Method }; } -interface RootMapMutations extends MapMutations { - (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; - >(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; +interface RootMapMutations extends MapMutations { + (namespace: string, map: Key[]): { [K in Key]: MutationMethod }; + >(namespace: string, map: Map): { [K in keyof Map]: MutationMethod }; >>(namespace: string, map: Map): { [K in keyof Map]: Method }; } /** * mapActions */ -interface MapActions { - (map: Key[]): { [K in Key]: ActionMethod }; - >(map: Map): { [K in keyof Map]: ActionMethod }; +interface MapActions { + (map: Key[]): { [K in Key]: ActionMethod }; + >(map: Map): { [K in keyof Map]: ActionMethod }; >>(map: Map): { [K in keyof Map]: Method }; } -interface RootMapActions extends MapActions { - (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; - >(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; +interface RootMapActions extends MapActions { + (namespace: string, map: Key[]): { [K in Key]: ActionMethod }; + >(namespace: string, map: Map): { [K in keyof Map]: ActionMethod }; >>(namespace: string, map: Map): { [K in keyof Map]: Method }; } /** * namespaced helpers */ -interface NamespacedMappers { +interface NamespacedMappers { mapState: MapState; mapGetters: MapGetters; - mapMutations: MapMutations; - mapActions: MapActions; + mapMutations: MapMutations; + mapActions: MapActions; } export declare const mapState: RootMapState; -export declare const mapMutations: RootMapMutations; +export declare const mapMutations: RootMapMutations; export declare const mapGetters: RootMapGetters; -export declare const mapActions: RootMapActions; +export declare const mapActions: RootMapActions; -export declare function createNamespacedHelpers(namespace?: string): NamespacedMappers; -export declare function createNamespacedHelpers(namespace?: string): NamespacedMappers; +export declare function createNamespacedHelpers(namespace?: string): NamespacedMappers; +export declare function createNamespacedHelpers(namespace?: string): NamespacedMappers; diff --git a/types/test/helpers.ts b/types/test/helpers.ts index cb22a9675..4d83599ac 100644 --- a/types/test/helpers.ts +++ b/types/test/helpers.ts @@ -142,6 +142,7 @@ new Vue({ this.g(7) this.h(8) this.i(9) + this.a() // should allow 0-argument call if untyped } }) @@ -202,5 +203,6 @@ new Vue({ this.h(8) this.i(9) this.otherMethod() + this.a() // should allow 0-argument call if untyped } }); From 9564b80735f7d08ad979838df2a2be3a09062fca Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 11 Jan 2018 20:53:07 +0900 Subject: [PATCH 11/23] docs(helpers): improve `createNamespacedHelpers` description --- docs/en/api.md | 2 +- src/helpers.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/api.md b/docs/en/api.md index df2d406fa..ef1aa9180 100644 --- a/docs/en/api.md +++ b/docs/en/api.md @@ -216,4 +216,4 @@ const store = new Vuex.Store({ ...options }) 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. + If the namespace is not specified, it returns the root mapXXX helpers. This is mainly for TypeScript users to annotate root helper's type. diff --git a/src/helpers.js b/src/helpers.js index 3f58d197f..e3cc43b5d 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -112,7 +112,7 @@ export const mapActions = normalizeNamespace((namespace, actions) => { /** * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object * If the namespace is not specified, it returns the root mapXXX helpers. - * This behavior is convenient to annotate strict types for mapXXX helpers. + * This is mainly for TypeScript users to annotate root helper's type. * @param {String} [namespace] * @return {Object} */ From 1aa407f03c99545cabdf38f284107ad430ae3820 Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 11 Jan 2018 20:54:48 +0900 Subject: [PATCH 12/23] fix(types): expose DefineGetters/Mutations/Actions type --- types/index.d.ts | 5 ++++- types/utils.d.ts | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index fe2e757a6..8fa9612fc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -12,7 +12,10 @@ export { } from "./helpers"; export { - DefineModule + DefineModule, + DefineGetters, + DefineMutations, + DefineActions } from './utils' export declare class Store { diff --git a/types/utils.d.ts b/types/utils.d.ts index 31d06d588..5d952aa8d 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -44,7 +44,7 @@ export interface DefineModule< * `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. */ -type DefineGetters< +export type DefineGetters< Getters, State, ExtraGetters = {}, @@ -65,7 +65,7 @@ type DefineGetters< * its corresponding values are payload types of the mutations. * `State` is a module state type which will be mutated in the mutations. */ -type DefineMutations = { +export type DefineMutations = { [K in keyof Mutations]: (state: State, payload: Mutations[K]) => void } @@ -78,7 +78,7 @@ type DefineMutations = { * `ExtraActions` is like `Actions` type but will be not defined in the infered actions object. * `RootState`, `RootGetters`, `RootMutations`, `RootActions` are the root module's asset types. */ -type DefineActions< +export type DefineActions< Actions, State, Getters, From cfb60429ddfe20ca01286e4c41ff887631f9ef24 Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 11 Jan 2018 23:31:43 +0900 Subject: [PATCH 13/23] chore: include utils.d.ts for `files` field --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index df90b03ae..26dad3826 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "dist", "types/index.d.ts", "types/helpers.d.ts", - "types/vue.d.ts" + "types/vue.d.ts", + "types/utils.d.ts" ], "scripts": { "dev": "node examples/server.js", From 00360b527413d68631b9c8551e661223d0ac5323 Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 18 Jan 2018 02:09:24 +0900 Subject: [PATCH 14/23] fix(types): make dispatch/commit more type safe in module actions if they are annotated --- types/index.d.ts | 58 ++++++++++++++----- types/test/index.ts | 2 + .../test/shopping-cart/store/modules/cart.ts | 4 +- types/utils.d.ts | 4 +- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 8fa9612fc..4cea493d1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -49,28 +49,65 @@ export declare class Store { export declare function install(Vue: typeof _Vue): void; -export interface Dispatch, RootActions = Record> { +/** + * Strict version of dispatch type. It always requires a payload. + */ +interface StrictDispatch, RootActions = Record> { // Local - (type: K, payload?: Actions[K], options?: LocalDispatchOptions): Promise; + (type: K, payload: Actions[K], options?: LocalDispatchOptions): Promise; (payloadWithType: InputPayload, options?: LocalDispatchOptions): Promise; // Root - (type: K, options: RootDispatchOptions): Promise; (type: K, payload: RootActions[K], options: RootDispatchOptions): Promise; (payloadWithType: InputPayload, options: RootDispatchOptions): Promise; } -export interface Commit, RootMutations = Record> { +/** + * Strict version of commit type. It always requires a payload. + */ +interface StrictCommit, RootMutations = Record> { // Local - (type: K, payload?: Mutations[K], options?: LocalCommitOptions): void; + (type: K, payload: Mutations[K], options?: LocalCommitOptions): void; (payloadWithType: InputPayload, options?: LocalCommitOptions): void; // Root - (type: K, options: RootCommitOptions): void; (type: K, payload: RootMutations[K], options: RootCommitOptions): void; (payloadWithType: InputPayload, options: RootCommitOptions): void; } +/** + * Loose dispatch type. It can omit a payload and may throw in run time + * since type checker cannot detect whether omitting payload is safe or not. + */ +export interface Dispatch, RootActions = Record> extends StrictDispatch { + // Local + (type: K): Promise; + + // Root + (type: K, options: RootDispatchOptions): Promise; +} + +/** + * Loose commit type. It can omit a payload and may throw in run time + * since type checker cannot detect whether omitting payload is safe or not. + */ +export interface Commit, RootMutations = Record> extends StrictCommit { + // Local + (type: K): void; + + // Root + (type: K, options: RootCommitOptions): void; +} + +export interface StrictActionContext { + dispatch: StrictDispatch; + commit: StrictCommit; + state: S; + getters: G; + rootState: RS; + rootGetters: RG; +} + export interface ActionContext< S, RS, @@ -80,14 +117,7 @@ export interface ActionContext< RM = Record, A = Record, RA = Record -> { - dispatch: Dispatch; - commit: Commit; - state: S; - getters: G; - rootState: RS; - rootGetters: RG; -} +> extends StrictActionContext {} export interface Payload { type: string; diff --git a/types/test/index.ts b/types/test/index.ts index 0253e5df8..57813e2b6 100644 --- a/types/test/index.ts +++ b/types/test/index.ts @@ -14,12 +14,14 @@ namespace StoreInstance { store.state.value; store.getters.foo; + store.dispatch("foo"); store.dispatch("foo", { amount: 1 }).then(() => {}); store.dispatch({ type: "foo", amount: 1 }).then(() => {}); + store.commit("foo"); store.commit("foo", { amount: 1 }); store.commit({ type: "foo", diff --git a/types/test/shopping-cart/store/modules/cart.ts b/types/test/shopping-cart/store/modules/cart.ts index ea2b417dd..df5221df4 100644 --- a/types/test/shopping-cart/store/modules/cart.ts +++ b/types/test/shopping-cart/store/modules/cart.ts @@ -68,10 +68,10 @@ export const cart: DefineModule commit('checkoutSuccess'), + () => commit('checkoutSuccess', null), () => commit('checkoutFailure', { savedCartItems }) ) }, diff --git a/types/utils.d.ts b/types/utils.d.ts index 5d952aa8d..cb5ccf609 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -1,4 +1,4 @@ -import { ActionContext, Module } from './index' +import { StrictActionContext, Module } from './index' /** * Type level utility to annotate types of module state/getters/actions/mutations (module assets). @@ -90,7 +90,7 @@ export type DefineActions< RootActions = {} > = { [K in keyof Actions]: ( - ctx: ActionContext, + ctx: StrictActionContext, payload: Actions[K] ) => Promise | void } From 9b89ae7f151b32cce5873290d8e70e693ba47503 Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 18 Jan 2018 02:11:27 +0900 Subject: [PATCH 15/23] fix(types): remove default type parameters from StrictDispatch/Commit --- types/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 4cea493d1..500da633a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -52,7 +52,7 @@ export declare function install(Vue: typeof _Vue): void; /** * Strict version of dispatch type. It always requires a payload. */ -interface StrictDispatch, RootActions = Record> { +interface StrictDispatch { // Local (type: K, payload: Actions[K], options?: LocalDispatchOptions): Promise; (payloadWithType: InputPayload, options?: LocalDispatchOptions): Promise; @@ -65,7 +65,7 @@ interface StrictDispatch, RootActions = Record, RootMutations = Record> { +interface StrictCommit { // Local (type: K, payload: Mutations[K], options?: LocalCommitOptions): void; (payloadWithType: InputPayload, options?: LocalCommitOptions): void; From 09475d511e439c956b10daee9ceab2ebe585ee8f Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 18 Jan 2018 02:14:18 +0900 Subject: [PATCH 16/23] refactor: use undefined type to indicate empty payload instead of null --- types/test/shopping-cart/store/modules/cart.ts | 8 ++++---- types/test/shopping-cart/store/modules/products.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/types/test/shopping-cart/store/modules/cart.ts b/types/test/shopping-cart/store/modules/cart.ts index df5221df4..ff259c674 100644 --- a/types/test/shopping-cart/store/modules/cart.ts +++ b/types/test/shopping-cart/store/modules/cart.ts @@ -28,8 +28,8 @@ export interface CartMutations { addToCart: { id: number }, - checkoutRequest: null, - checkoutSuccess: null, + checkoutRequest: undefined, + checkoutSuccess: undefined, checkoutFailure: { savedCartItems: AddedItem[] } @@ -68,10 +68,10 @@ export const cart: DefineModule commit('checkoutSuccess', null), + () => commit('checkoutSuccess', undefined), () => commit('checkoutFailure', { savedCartItems }) ) }, diff --git a/types/test/shopping-cart/store/modules/products.ts b/types/test/shopping-cart/store/modules/products.ts index c55248438..80ab971bf 100644 --- a/types/test/shopping-cart/store/modules/products.ts +++ b/types/test/shopping-cart/store/modules/products.ts @@ -17,7 +17,7 @@ export interface ProductsGetters { } export interface ProductsActions { - getAllProducts: null + getAllProducts: undefined } export interface ProductsMutations { From 8e0c60b4a83579b792752d3e9658328d0b2cf8a2 Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 18 Jan 2018 02:15:32 +0900 Subject: [PATCH 17/23] fix(types): fix incorrect type annotation --- types/test/shopping-cart/store/modules/products.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/test/shopping-cart/store/modules/products.ts b/types/test/shopping-cart/store/modules/products.ts index 80ab971bf..8eb28fe4d 100644 --- a/types/test/shopping-cart/store/modules/products.ts +++ b/types/test/shopping-cart/store/modules/products.ts @@ -29,7 +29,7 @@ export interface ProductsMutations { } } -export const productsHelpers = createNamespacedHelpers('products') +export const productsHelpers = createNamespacedHelpers('products') export const products: DefineModule = { namespaced: true, From b14662c524edbf7d669993bd25c87aff32523f5a Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 18 Jan 2018 02:28:14 +0900 Subject: [PATCH 18/23] fix(types): fix ActionContext type --- types/index.d.ts | 5 ++++- types/test/index.ts | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 500da633a..4d4c70e21 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -117,7 +117,10 @@ export interface ActionContext< RM = Record, A = Record, RA = Record -> extends StrictActionContext {} +> extends StrictActionContext { + dispatch: Dispatch; + commit: Commit; +} export interface Payload { type: string; diff --git a/types/test/index.ts b/types/test/index.ts index 57813e2b6..c19a28277 100644 --- a/types/test/index.ts +++ b/types/test/index.ts @@ -63,8 +63,18 @@ namespace RootModule { foo ({ state, getters, dispatch, commit }, payload) { state.value; getters.count; - dispatch("bar", {}); - commit("bar", {}); + dispatch("bar"); + dispatch("bar", { value: 1 }); + dispatch({ + type: "bar", + value: 1 + }); + commit("bar"); + commit("bar", { value: 1 }); + commit({ + type: "bar", + value: 1 + }); } }, mutations: { From b24d744bf05b69ba36089163410b943de65d6a9b Mon Sep 17 00:00:00 2001 From: ktsn Date: Thu, 18 Jan 2018 23:56:26 +0900 Subject: [PATCH 19/23] fix(types): remove incorrect overload --- types/index.d.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 4d4c70e21..49362bb08 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -80,11 +80,7 @@ interface StrictCommit { * since type checker cannot detect whether omitting payload is safe or not. */ export interface Dispatch, RootActions = Record> extends StrictDispatch { - // Local (type: K): Promise; - - // Root - (type: K, options: RootDispatchOptions): Promise; } /** @@ -92,11 +88,7 @@ export interface Dispatch, RootActions = Record, RootMutations = Record> extends StrictCommit { - // Local (type: K): void; - - // Root - (type: K, options: RootCommitOptions): void; } export interface StrictActionContext { From 8b6a6f919a553f2342d6f839491c0e26be7449d8 Mon Sep 17 00:00:00 2001 From: ktsn Date: Fri, 19 Jan 2018 00:02:10 +0900 Subject: [PATCH 20/23] refactor(types): just use Dispatch/CommitOptions instead of BaseDispatch/CommitOptions --- types/index.d.ts | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 49362bb08..980273085 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -128,35 +128,28 @@ export interface ActionPayload extends Payload { payload: any; } -interface BaseDispatchOptions {} - -interface LocalDispatchOptions extends BaseDispatchOptions { - root?: false -} - -interface RootDispatchOptions extends BaseDispatchOptions { - root: true +interface LocalDispatchOptions extends DispatchOptions { + root?: false; } -interface BaseCommitOptions { - silent?: boolean +interface RootDispatchOptions extends DispatchOptions { + root: true; } -interface LocalCommitOptions extends BaseCommitOptions { - root?: false +interface LocalCommitOptions extends CommitOptions { + root?: false; } -interface RootCommitOptions extends BaseCommitOptions { - root: true +interface RootCommitOptions extends CommitOptions { + root: true; } -// Leave for backward compatibility -export interface DispatchOptions extends BaseDispatchOptions { +export interface DispatchOptions { root?: boolean; } -// Leave for backward compatibility -export interface CommitOptions extends BaseCommitOptions { +export interface CommitOptions { + silent?: boolean; root?: boolean; } From 9b183f092b93dd00fdd413f8fc14ad9426b48896 Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 23 Jan 2018 00:06:15 +0900 Subject: [PATCH 21/23] docs: add typescript docs --- docs/en/SUMMARY.md | 1 + docs/en/typescript.md | 228 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 docs/en/typescript.md diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index ce35718c0..341645938 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -18,4 +18,5 @@ - [Form Handling](forms.md) - [Testing](testing.md) - [Hot Reloading](hot-reload.md) +- [TypeScript Support](typescript.md) - [API Reference](api.md) diff --git a/docs/en/typescript.md b/docs/en/typescript.md new file mode 100644 index 000000000..7713c68e9 --- /dev/null +++ b/docs/en/typescript.md @@ -0,0 +1,228 @@ +# TypeScript Support + +## Utility Types for Modules + +Vuex provides some utility types to help you to declare modules in TypeScript. They avoid runtime errors when using state, getters, mutations and actions in a module thanks to type checking. + +To use the utility types, you should declare module assets types at first. Following is a simple example of counter module types: + +```ts +// State type +export interface CounterState { + count: number +} + +// Getters type +// key: getter name +// value: return type of getter +export interface CounterGetters { + power: number +} + +// Mutations type +// key: mutation name +// value: payload type of mutation +export interface CounterMutations { + increment: { amount: number } +} + +// Actions type +// key: action name +// value: payload type of action +export interface CounterActions { + incrementAsync: { amount: number, delay: number } +} +``` + +The state type must describe an actual state shape. The `CounterState` in the example indicates that the module's state has `count` property which must fulfill `number` type. + +The getters type describes what getter names exist in the module according to keys. The corresponding value type shows what type the getter returns. The `CounterGetters` in the example indicates that the module has a getter named `power` and it returns a value of type `number`. + +Both the actions and mutations type describe what thier names exist in the module as same as getters type. The value type of them indicates the payload type. The `CounterMutations` illustrates that the module has `increment` mutation and its payload is an object having `amount` property of type `number`, while the `CounterActions` shows there is `incrementAsync` action with an object payload having `amount` and `delay` property of type `number` in the module. + +After declare the module assets types, you import `DefineModule` utility type and annotate the module with it: + +```ts +import { DefineModule } from 'vuex' + +// Implementation of counter module +export const counter: DefineModule = { + namespaced: true, + + // Follow CounterState + state: { + count: 0 + }, + + // Follow CounterGetters + getters: { + power: state => state.count * state.count + }, + + // Follow CounterMutations + mutations: { + increment (state, payload) { + state.count += payload.amount + } + }, + + // Follow CounterActions + actions: { + incrementAsync ({ commit }, payload) { + setTimeout(() => { + commit('increment', { amount: payload.amount }) + }, payload.delay) + } + } +} +``` + +Note that all function arguments types are infered without manually annotating them including `dispatch` and `commit` in the action context. If you try to dispach an action (commit a mutation) that does not exist or the payload type is not valid on the declared types, it throws a compilation error: + +### Using external modules in the same namespace + +Sometimes you may want to use external modules' getters, actions and mutations in the same namespace. In that case, you can pass the external module assets types to `DefineModule` generic parameters to extend the module type: + +```ts +// External module assets types +// You may import them from another file on a practical code +interface ExternalGetters { + extraValue: number +} + +interface ExternalMutations { + loading: boolean +} + +interface ExternalActions { + sendTrackingData: { name: string, value: string } +} + +export const counter: DefineModule< + // The first 4 type parameters are for module assets + CounterState, + CounterGetters, + CounterMutations, + CounterActions, + + // 3 type parameters that follows the module assets types are external module assets types + ExternalGetters, + ExternalMutations, + ExternalActions +> = { + namespaced: true, + + state: { /* ... */ }, + mutations: { /* ... */ }, + + getters: { + power (state, getters) { + // You can use a getter from the external module + console.log(getters.extraValue) + return state.count * state.count + } + }, + + actions: { + incrementAsync ({ commit, dispatch }, payload) { + // Using the external action + dispatch('sendTrackingData', { + name: 'increment', + value: payload.amount + }) + + // Using the external mutation + commit('loading', true) + setTimeout(() => { + commit('increment', { amount: payload.amount }) + commit('loading', false) + }, payload.delay) + } + } +} +``` + +### Using the root state, getters, actions and mutations + +If you want to use root state, getters, actions and mutations, you can pass root assets types following external assets types on `DefineModule`: + +```ts +export const counter: DefineModule< + CounterState, + CounterGetters, + CounterMutations, + CounterActions, + + // You can use `{}` type if you will not use them + {}, // External getters + {}, // External mutations + {}, // External actions + + // Root types can be specified after external assets types + RootState, + RootGetters, + RootMutations, + RootActions +> = { + /* ... module implementation ... */ +} +``` + +## Typed Component Binding Helpers + +You probably want to use fully typed `state`, `getters`, `dispatch` and `commit` not only in modules but also from components. You can use `createNamespacedHelpers` to use typed module assets on components. The `createNamespacedHelpers` accepts 4 generic parameters to annotate returned `mapState`, `mapGetters`, `mapMutations` and `mapActions` by using module assets types: + +```ts +export const counterHelpers = createNamespacedHelpers('counter') +``` + +All the returned helpers and mapped computed properties and methods will be type checked. You can use them without concerning typos and invalid payload by yourself: + +```ts +export default Vue.extend({ + computed: counterHelpers.mapState({ + value: 'count' + }), + + methods: counterHelpers.mapMutations({ + inc: 'increment' + }), + + created () { + // These are correctly typed! + this.inc({ amount: 1 }) + console.log(this.value) + } +}) +``` + +### Annotating Root Binding Helpers + +`createNamespacedHelpers` is made for generating new component binding helpers focusing a namespaced module. The API however is useful to create typed root binding helpers. So if you need them, you call `createNamespacedHelpers` without passing namespace: + +```ts +const rootHelpers = createNamespacedHelpers() +``` + +## Explicit Payload + +While regular (not strictly typed) `dispatch` and `commit` can omit a payload, typed ones does not allow to omit its payload. This is because to ensure type safety of a payload. If you want to declare actions / mutations that do not have a payload you should explicitly pass `undefined` value. + +```ts +export interface CounterMutation { + // This indicates the `increment` action does not have a payload + increment: undefined +} + +// ... +export const counter: DefineModule = { + // ... + + actions: { + someAction ({ commit }) { + // Passing `undefined` value explicitly + commit('increment', undefined) + } + } +} +``` From 715eaad945bb44ad10cd1f2136ed901470a72713 Mon Sep 17 00:00:00 2001 From: ktsn Date: Tue, 23 Jan 2018 00:15:21 +0900 Subject: [PATCH 22/23] docs: fix typo in typescript docs --- docs/en/typescript.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/typescript.md b/docs/en/typescript.md index 7713c68e9..265a4c38c 100644 --- a/docs/en/typescript.md +++ b/docs/en/typescript.md @@ -40,7 +40,7 @@ The getters type describes what getter names exist in the module according to ke Both the actions and mutations type describe what thier names exist in the module as same as getters type. The value type of them indicates the payload type. The `CounterMutations` illustrates that the module has `increment` mutation and its payload is an object having `amount` property of type `number`, while the `CounterActions` shows there is `incrementAsync` action with an object payload having `amount` and `delay` property of type `number` in the module. -After declare the module assets types, you import `DefineModule` utility type and annotate the module with it: +After declaring the module assets types, you import `DefineModule` utility type and annotate the module with it: ```ts import { DefineModule } from 'vuex' @@ -77,7 +77,7 @@ export const counter: DefineModule Date: Thu, 25 Jan 2018 19:28:36 +0900 Subject: [PATCH 23/23] docs: add a note about conditional types --- docs/en/typescript.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/typescript.md b/docs/en/typescript.md index 265a4c38c..42917b481 100644 --- a/docs/en/typescript.md +++ b/docs/en/typescript.md @@ -226,3 +226,5 @@ export const counter: DefineModule