├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.txt ├── LICENSE ├── README.md ├── build └── make-bundles.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src └── direct-vuex.ts ├── tests ├── action-context.spec.ts ├── namespaced.spec.ts └── non-namespaced.spec.ts ├── tsconfig.json ├── tslint.json └── types ├── direct-types.d.ts └── index.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ._* 3 | 4 | node_modules 5 | .npmrc 6 | 7 | /build/compiled-* 8 | /dist 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | node_js: 4 | - 10 5 | - 12 6 | cache: 7 | directories: 8 | - "node_modules" 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll": true, 5 | "source.organizeImports": true 6 | }, 7 | "editor.defaultFormatter": "vscode.typescript-language-features", 8 | "editor.formatOnSave": true 9 | }, 10 | "editor.insertSpaces": true, 11 | "editor.rulers": [120], 12 | "editor.tabSize": 2, 13 | "editor.wordWrap": "wordWrapColumn", 14 | "editor.wordWrapColumn": 132, 15 | "files.encoding": "utf8", 16 | "files.trimTrailingWhitespace": true, 17 | "javascript.format.semicolons": "remove", 18 | "search.useIgnoreFiles": true, 19 | "typescript.locale": "en", 20 | "typescript.preferences.importModuleSpecifier": "relative", 21 | "typescript.preferences.quoteStyle": "double", 22 | "typescript.tsdk": "node_modules/typescript/lib" 23 | } 24 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.12.0 2 | * Changed state and getters to readonly. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # direct-vuex 2 | 3 | [![Build Status](https://travis-ci.com/paroi-tech/direct-vuex.svg?branch=master)](https://travis-ci.com/paroi-tech/direct-vuex) 4 | [![Dependencies Status](https://david-dm.org/paroi-tech/direct-vuex/status.svg)](https://david-dm.org/paroi-tech/direct-vuex) 5 | [![npm](https://img.shields.io/npm/dm/direct-vuex)](https://www.npmjs.com/package/direct-vuex) 6 | ![Type definitions](https://img.shields.io/npm/types/direct-vuex) 7 | [![GitHub](https://img.shields.io/github/license/paroi-tech/direct-vuex)](https://github.com/paroi-tech/direct-vuex) 8 | 9 | Use and implement your Vuex store with TypeScript types. Direct-vuex doesn't require classes, therefore it is compatible with the Vue 3 composition API. 10 | 11 | ## Install 12 | 13 | First, add `direct-vuex` to a **Vue 2** application: 14 | 15 | ```sh 16 | npm install direct-vuex 17 | ``` 18 | 19 | Or, in a **Vue 3** application: 20 | 21 | ```sh 22 | npm install direct-vuex@next 23 | ``` 24 | 25 | ## Create the store 26 | 27 | The store can be implemented almost in the same way as usual. 28 | 29 | Create the store: 30 | 31 | ```ts 32 | import Vue from "vue" 33 | import Vuex from "vuex" 34 | import { createDirectStore } from "direct-vuex" 35 | 36 | Vue.use(Vuex) 37 | 38 | const { 39 | store, 40 | rootActionContext, 41 | moduleActionContext, 42 | rootGetterContext, 43 | moduleGetterContext 44 | } = createDirectStore({ 45 | // … store implementation here … 46 | }) 47 | 48 | // Export the direct-store instead of the classic Vuex store. 49 | export default store 50 | 51 | // The following exports will be used to enable types in the 52 | // implementation of actions and getters. 53 | export { 54 | rootActionContext, 55 | moduleActionContext, 56 | rootGetterContext, 57 | moduleGetterContext 58 | } 59 | 60 | // The following lines enable types in the injected store '$store'. 61 | export type AppStore = typeof store 62 | declare module "vuex" { 63 | interface Store { 64 | direct: AppStore 65 | } 66 | } 67 | ``` 68 | 69 | The classic Vuex store is still accessible through the `store.original` property. We need it to initialize the Vue application: 70 | 71 | ```ts 72 | import Vue from "vue" 73 | import store from "./store" 74 | 75 | new Vue({ 76 | store: store.original, // Inject the classic Vuex store. 77 | // … 78 | }).$mount("#app") 79 | ``` 80 | 81 | ## Use typed wrappers from outside the store 82 | 83 | From a component, the direct store is accessible through the `direct` property of the classic store: 84 | 85 | ```ts 86 | const store = context.root.$store.direct // or: this.$store.direct 87 | ``` 88 | 89 | Or, you can just import it: 90 | 91 | ```ts 92 | import store from "./store" 93 | ``` 94 | 95 | Then, the old way to call an action: 96 | 97 | ```ts 98 | store.dispatch("mod1/myAction", myPayload) 99 | ``` 100 | 101 | … is replaced by the following wrapper: 102 | 103 | ```ts 104 | store.dispatch.mod1.myAction(myPayload) 105 | ``` 106 | 107 | … which is fully typed. 108 | 109 | Typed getters and mutations are accessible the same way: 110 | 111 | ```ts 112 | store.getters.mod1.myGetter 113 | store.commit.mod1.myMutation(myPayload) 114 | ``` 115 | 116 | Notice: The underlying Vuex store can be used simultaneously if you wish, through the injected `$store` or `store.original`. 117 | 118 | ## A limitation on how to declare a State 119 | 120 | In store and module options, the `state` property shouldn't be declared with the ES6 method syntax. 121 | 122 | Valid: 123 | 124 | ```ts 125 | state: { p1: string } as Mod1State 126 | ``` 127 | 128 | ```ts 129 | state: (): Mod1State => { p1: string } 130 | ``` 131 | 132 | ```ts 133 | state: function (): Mod1State { return { p1: string } } 134 | ``` 135 | 136 | Invalid: 137 | 138 | ```ts 139 | state(): Mod1State { return { p1: string } } 140 | ``` 141 | 142 | I'm not sure why but TypeScript doesn't infer the state type correctly when we write that. 143 | 144 | ## Implement a Vuex Store with typed helpers 145 | 146 | Direct-vuex provides several useful helpers for implementation of the store. They are all optional. However, if you want to keep your classic implementation of a Vuex Store, then direct-vuex needs to infer the literal type of the `namespaced` property. You can write `namespaced: true as true` where there is a `namespaced` property. But you don't need to worry about that if you use `defineModule`. 147 | 148 | ### In a Vuex Module 149 | 150 | The function `defineModule` is provided solely for type inference. It is a no-op behavior-wise. It expects a module implementation and returns the argument as-is. This behaviour is similar to (and inspired from) the [function `defineComponent`](https://vue-composition-api-rfc.netlify.com/api.html#definecomponent) from the composition API. 151 | 152 | The generated functions `moduleActionContext` and `moduleGetterContext` are factories for creating functions `mod1ActionContext` and `mod1GetterContext`, which converts injected action and getter contexts to their direct-vuex equivalent. 153 | 154 | Here is how to use `defineModule`, `moduleActionContext` and `moduleGetterContext`: 155 | 156 | ```ts 157 | import { defineModule } from "direct-vuex" 158 | import { moduleActionContext, moduleGetterContext } from "./store" 159 | 160 | export interface Mod1State { 161 | p1: string 162 | } 163 | 164 | const mod1 = defineModule({ 165 | state: (): Mod1State => { 166 | return { 167 | p1: "" 168 | } 169 | }, 170 | getters: { 171 | p1OrDefault(...args): string { 172 | const { state, getters, rootState, rootGetters } = mod1GetterContext(args) 173 | // Here, 'getters', 'state', 'rootGetters' and 'rootState' are typed. 174 | // Without 'mod1GetterContext' only 'state' would be typed. 175 | return state.p1 || "default" 176 | } 177 | }, 178 | mutations: { 179 | SET_P1(state, p1: string) { 180 | // Here, the type of 'state' is 'Mod1State'. 181 | state.p1 = p1 182 | } 183 | }, 184 | actions: { 185 | loadP1(context, payload: { id: string }) { 186 | const { dispatch, commit, getters, state } = mod1ActionContext(context) 187 | // Here, 'dispatch', 'commit', 'getters' and 'state' are typed. 188 | } 189 | }, 190 | }) 191 | 192 | export default mod1 193 | const mod1GetterContext = (args: [any, any, any, any]) => moduleGetterContext(args, mod1) 194 | const mod1ActionContext = (context: any) => moduleActionContext(context, mod1) 195 | ``` 196 | 197 | 2 Warnings: 198 | 199 | * Types in the context of actions implies that TypeScript should never infer the return type of an action from the context of the action. Indeed, this kind of typing would be recursive, since the context includes the return value of the action. When this happens, TypeScript passes the whole context to `any`. Tl;dr; **Declare the return type of actions where it exists!** 200 | * For the same reason, **declare the return type of getters each time a getter context generated by `moduleGetterContext` is used!** 201 | 202 | ### Get the typed context of a Vuex Getter, but in the root store 203 | 204 | The generated function `rootGetterContext` converts the injected action context to the direct-vuex one, at the root level (not in a module). 205 | 206 | ```ts 207 | getters: { 208 | getterInTheRootStore(...args) { 209 | const { state, getters } = rootGetterContext(args) 210 | // Here, 'getters', 'state' are typed. 211 | // Without 'rootGetterContext' only 'state' would be typed. 212 | } 213 | } 214 | ``` 215 | 216 | ### Get the typed context of a Vuex Action, but in the root store 217 | 218 | The generated function `rootActionContext` converts the injected action context to the direct-vuex one, at the root level (not in a module). 219 | 220 | ```ts 221 | actions: { 222 | async actionInTheRootStore(context, payload) { 223 | const { commit, state } = rootActionContext(context) 224 | // … Here, 'commit' and 'state' are typed. 225 | } 226 | } 227 | ``` 228 | 229 | ### Alternatively: Use `localGetterContext` and `localActionContext` 230 | 231 | Instead of `moduleActionContext` and `moduleGetterContext`, which imply circular dependencies, it is possible to use `localGetterContext` and `localActionContext`: 232 | 233 | ```ts 234 | import { defineModule, localActionContext, localGetterContext } from "direct-vuex" 235 | 236 | const mod1 = defineModule({ 237 | // … 238 | }) 239 | 240 | export default mod1 241 | const mod1GetterContext = (args: [any, any, any, any]) => localGetterContext(args, mod1) 242 | const mod1ActionContext = (context: any) => localActionContext(context, mod1) 243 | ``` 244 | 245 | Now there isn't circular dependency, but getter and action contexts don't provide access to `rootState`, `rootGetters`, `rootCommit`, `rootDispatch`. 246 | 247 | Functions `localGetterContext` and `localActionContext` can be used in place of `rootGetterContext` and `rootActionContext` too. 248 | 249 | ### Use `defineGetters` 250 | 251 | The function `defineGetters` is provided solely for type inference. It is a no-op behavior-wise. It is a factory for a function, which expects the object of a `getters` property and returns the argument as-is. 252 | 253 | ```ts 254 | import { defineGetters } from "direct-vuex" 255 | import { Mod1State } from "./mod1" // Import the local definition of the state (for example from the current module) 256 | 257 | export default defineGetters()({ 258 | getter1(...args) { 259 | const { state, getters, rootState, rootGetters } = mod1GetterContext(args) 260 | // Here, 'getters', 'state', 'rootGetters' and 'rootState' are typed. 261 | // Without 'mod1GetterContext' only 'state' would be typed. 262 | }, 263 | }) 264 | ``` 265 | 266 | Note: There is a limitation. The second parameters `getters` in a getter implementation, is not typed. 267 | 268 | ### Use `defineMutations` 269 | 270 | The function `defineMutations` is provided solely for type inference. It is a no-op behavior-wise. It is a factory for a function, which expects the object of a `mutations` property and returns the argument as-is. 271 | 272 | ```ts 273 | import { defineMutations } from "direct-vuex" 274 | import { Mod1State } from "./mod1" // Import the local definition of the state (for example from the current module) 275 | 276 | export default defineMutations()({ 277 | SET_P1(state, p1: string) { 278 | // Here, the type of 'state' is 'Mod1State'. 279 | state.p1 = p1 280 | } 281 | }) 282 | ``` 283 | 284 | ### Use `defineActions` 285 | 286 | The function `defineActions` is provided solely for type inference. It is a no-op behavior-wise. It expects the object of an `actions` property and returns the argument as-is. 287 | 288 | ```ts 289 | import { defineActions } from "direct-vuex" 290 | 291 | export default defineActions({ 292 | loadP1(context, payload: { id: string }) { 293 | const { dispatch, commit, getters, state } = mod1ActionContext(context) 294 | // Here, 'dispatch', 'commit', 'getters' and 'state' are typed. 295 | } 296 | }) 297 | ``` 298 | 299 | ## About Direct-vuex and Circular Dependencies 300 | 301 | When the helper `moduleActionContext` and `moduleGetterContext` are used, linters may warn about an issue: _"Variable used before it's assigned"_. I couldn't avoid circular dependencies. Action contexts and getter contexts need to be inferred at the store level, because they contain `rootState` etc. 302 | 303 | Here is an example of a Vuex module implementation: 304 | 305 | ```ts 306 | import { moduleActionContext } from "./store" 307 | 308 | const mod1 = { 309 | getters: { 310 | p1OrDefault(...args) { 311 | const { state, getters, rootState, rootGetters } = mod1GetterContext(args) 312 | // … 313 | } 314 | }, 315 | actions: { 316 | loadP1(context, payload: { id: string }) { 317 | const { commit, rootState } = mod1ActionContext(context) 318 | // … 319 | } 320 | } 321 | } 322 | 323 | export default mod1 324 | const mod1ActionContext = (context: any) => moduleActionContext(context, mod1) 325 | const mod1GetterContext = (args: [any, any, any, any]) => moduleGetterContext(args, mod1) 326 | ``` 327 | 328 | It works because `mod1ActionContext` (or `mod1GetterContext`) is not executed at the same time it is declared. It is executed when an action (or a getter) is executed, ie. after all the store and modules are already initialized. 329 | 330 | I suggest to disable the linter rule with a comment at the top of the source file. 331 | 332 | With TSLint: 333 | 334 | ```js 335 | // tslint:disable: no-use-before-declare 336 | ``` 337 | 338 | With ESLint: 339 | 340 | ```js 341 | /* eslint-disable no-use-before-define */ 342 | ``` 343 | 344 | **Notice: A consequence of these circular dependencies is that _the main store file must be imported first_ from the rest of the application. If a Vuex module is imported first, some part of your implementation could be `undefined` at runtime.** 345 | 346 | ## Contribute 347 | 348 | With VS Code, our recommended plugin is: 349 | 350 | * **TSLint** from Microsoft (`ms-vscode.vscode-typescript-tslint-plugin`) 351 | -------------------------------------------------------------------------------- /build/make-bundles.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require("fs") 2 | const { writeFile, mkdir } = require("fs").promises 3 | const { join, resolve } = require("path") 4 | const { rollup } = require("rollup") 5 | const terser = require("terser") 6 | 7 | const packagePath = resolve(__dirname, "..") 8 | const distNpmPath = join(packagePath, "dist") 9 | 10 | async function build() { 11 | if (!existsSync(distNpmPath)) 12 | await mkdir(distNpmPath) 13 | await makeBundle(join(__dirname, "compiled-esm", "direct-vuex.js"), "direct-vuex.esm", "esm") 14 | await makeBundle(join(__dirname, "compiled-es5", "direct-vuex.js"), "direct-vuex.umd", "umd") 15 | } 16 | 17 | async function makeBundle(mainFile, bundleName, format) { 18 | const bundle = await rollup({ 19 | input: mainFile, 20 | context: "this" // preserve 'this' in TS's ES5 helpers 21 | }) 22 | const { output } = await bundle.generate({ 23 | format, 24 | name: "DirectVuex", 25 | sourcemap: false, 26 | exports: "named", 27 | globals: { 28 | vuex: "Vuex" // global variable name for UMD and System 29 | }, 30 | }) 31 | const bundleCode = output[0].code 32 | const minified = terser.minify({ 33 | bundle: bundleCode 34 | }) 35 | if (minified.error) 36 | throw minified.error 37 | 38 | await writeFile(join(distNpmPath, `${bundleName}.min.js`), minified.code) 39 | await writeFile(join(distNpmPath, `${bundleName}.js`), bundleCode) 40 | } 41 | 42 | build().catch(err => console.log(err.message, err.stack)) 43 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | transform: { 5 | "^.+\\.ts$": "ts-jest", 6 | }, 7 | moduleFileExtensions: [ 8 | "js", 9 | "json", 10 | "ts", 11 | ], 12 | testMatch: [ 13 | "**/(src|tests)/**/*.spec.(js|ts)", 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "direct-vuex", 3 | "version": "1.0.0-rc3", 4 | "description": "Use and implement your Vuex store with TypeScript types. Compatible with the Vue 3 composition API.", 5 | "author": "Paleo", 6 | "files": [ 7 | "dist", 8 | "types" 9 | ], 10 | "main": "dist/direct-vuex.umd.js", 11 | "module": "dist/direct-vuex.esm.js", 12 | "types": "types/index.d.ts", 13 | "scripts": { 14 | "prepublishOnly": "npm run test", 15 | "prepare": "npm run build", 16 | "clear": "rimraf 'build/compiled-*/*'", 17 | "tsc": "tsc", 18 | "tsc-es5": "tsc --target ES5 --outDir build/compiled-es5", 19 | "tsc:watch": "tsc --watch", 20 | "make-bundles": "node build/make-bundles.js", 21 | "build": "npm run clear && npm run tsc && npm run tsc-es5 && npm run make-bundles", 22 | "lint": "tslint -p tsconfig.json -t verbose", 23 | "test": "jest", 24 | "test:watch": "jest --watch" 25 | }, 26 | "peerDependencies": { 27 | "vue": "3", 28 | "vuex": "4" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^26.0.20", 32 | "jest": "^26.6.3", 33 | "rimraf": "^3.0.2", 34 | "rollup": "^2.39.1", 35 | "terser": "^5.6.0", 36 | "ts-jest": "^26.5.2", 37 | "tslint": "^6.1.3", 38 | "typescript": "^4.2.2", 39 | "vue": "^3.0.5", 40 | "vuex": "^4.0.0" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/paroi-tech/direct-vuex.git" 45 | }, 46 | "license": "CC0-1.0", 47 | "keywords": [ 48 | "vuex", 49 | "typescript" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/direct-vuex.ts: -------------------------------------------------------------------------------- 1 | import { ActionContext, createStore, Store } from "vuex" 2 | import { ActionsImpl, GettersImpl, ModuleOptions, ModulesImpl, MutationsImpl, StateOf, StoreOptions, StoreOrModuleOptions, WithOptionalState } from "../types" 3 | import { CreatedStore, ToDirectStore, VuexStore } from "../types/direct-types" 4 | 5 | export function createDirectStore< 6 | O extends WithOptionalState, 7 | S = StateOf 8 | >(options: O & StoreOptions): CreatedStore { 9 | const original = createStore(options as any) as VuexStore 10 | 11 | const store: ToDirectStore = { 12 | get state() { 13 | return original.state as any 14 | }, 15 | getters: toDirectGetters(options, original.getters), 16 | commit: toDirectCommit(options, original.commit), 17 | dispatch: toDirectDispatch(options, original.dispatch), 18 | original 19 | } 20 | 21 | original.direct = store 22 | 23 | return { 24 | store, 25 | rootGetterContext: 26 | ([state, getters]: [any, any]) => getModuleGetterContext([state, getters, state, getters], options, options), 27 | moduleGetterContext: 28 | (args: [any, any, any, any], moduleOptions: any) => 29 | getModuleGetterContext(args, moduleOptions, options), 30 | rootActionContext: (originalContext: any) => getModuleActionContext(originalContext, options, options), 31 | moduleActionContext: 32 | (originalContext: any, moduleOptions: any) => getModuleActionContext(originalContext, moduleOptions, options), 33 | } 34 | } 35 | 36 | export function localGetterContext( 37 | [state, getters]: [any, any, ...any[]], options: O 38 | ): any { 39 | return getModuleGetterContext([state, getters, undefined, undefined], options) 40 | } 41 | 42 | export function localActionContext( 43 | originalContext: ActionContext, 44 | options: O 45 | ): any { 46 | return getModuleActionContext(originalContext, options) 47 | } 48 | 49 | export function defineModule< 50 | O extends WithOptionalState, 51 | S = StateOf 52 | >(options: O & ModuleOptions): O { 53 | return options 54 | } 55 | 56 | export function defineModules(): ((modules: T & ModulesImpl) => T) { 57 | return modules => modules 58 | } 59 | 60 | export function defineGetters(): ((getters: T & GettersImpl) => T) { 61 | return getters => getters 62 | } 63 | 64 | export function defineMutations(): ((mutations: T & MutationsImpl) => T) { 65 | return mutations => mutations 66 | } 67 | 68 | export function defineActions(actions: T & ActionsImpl): T { 69 | return actions 70 | } 71 | 72 | export const createModule = obsolete(defineModule, "createModule", "defineModule") 73 | export const createModules = obsolete(defineModules, "createModules", "defineModules") 74 | export const createGetters = obsolete(defineGetters, "createGetters", "defineGetters") 75 | export const createMutations = obsolete(defineMutations, "createMutations", "defineMutations") 76 | export const createActions = obsolete(defineActions, "createActions", "defineActions") 77 | 78 | function obsolete any>(fn: T, oldName: string, newName: string): T { 79 | return ((...args) => { 80 | // tslint:disable-next-line:no-console 81 | console.warn(`Function '${oldName}' is obsolete, please use '${newName}'.`) 82 | return fn(...args) 83 | }) as T 84 | } 85 | 86 | export default { 87 | createDirectStore, defineModule, defineModules, defineGetters, defineMutations, defineActions, 88 | localGetterContext, localActionContext, 89 | createModule, createModules, createGetters, createMutations, createActions 90 | } 91 | 92 | // Getters 93 | 94 | const gettersCache = new WeakMap["getters"], any>() 95 | 96 | function toDirectGetters(options: StoreOrModuleOptions, originalGetters: Store["getters"]) { 97 | let getters = gettersCache.get(originalGetters) 98 | // console.log(">> to-getters", getters ? "FROM_CACHE" : "CREATE", options) 99 | if (!getters) { 100 | getters = gettersFromOptions({}, options, originalGetters) 101 | gettersCache.set(originalGetters, getters) 102 | } 103 | return getters 104 | } 105 | 106 | function gettersFromOptions( 107 | result: any, 108 | options: StoreOrModuleOptions, 109 | originalGetters: Store["getters"], 110 | hierarchy: string[] = [] 111 | ): any { 112 | if (options.getters) 113 | createDirectGetters(result, options.getters, originalGetters, hierarchy) 114 | if (options.modules) { 115 | for (const moduleName of Object.keys(options.modules)) { 116 | const moduleOptions = options.modules[moduleName] 117 | if (moduleOptions.namespaced) 118 | result[moduleName] = gettersFromOptions({}, moduleOptions, originalGetters, [...hierarchy, moduleName]) 119 | else 120 | gettersFromOptions(result, moduleOptions, originalGetters, hierarchy) 121 | } 122 | } 123 | return result 124 | } 125 | 126 | function createDirectGetters( 127 | result: any, 128 | gettersImpl: GettersImpl, 129 | originalGetters: Store["getters"], 130 | hierarchy?: string[] 131 | ) { 132 | const prefix = !hierarchy || hierarchy.length === 0 ? "" : `${hierarchy.join("/")}/` 133 | for (const name of Object.keys(gettersImpl)) { 134 | Object.defineProperties(result, { 135 | [name]: { 136 | get: () => originalGetters[`${prefix}${name}`] 137 | } 138 | }) 139 | } 140 | } 141 | 142 | // Mutations 143 | 144 | const commitCache = new WeakMap["commit"], any>() 145 | 146 | function toDirectCommit(options: StoreOrModuleOptions, originalCommit: Store["commit"]) { 147 | let commit = commitCache.get(originalCommit) 148 | // console.log(">> to-commit", commit ? "FROM_CACHE" : "CREATE", options) 149 | if (!commit) { 150 | commit = commitFromOptions({}, options, originalCommit) 151 | commitCache.set(originalCommit, commit) 152 | } 153 | return commit 154 | } 155 | 156 | const rootCommitCache = new WeakMap["commit"], any>() 157 | 158 | function toDirectRootCommit(rootOptions: StoreOptions, originalCommit: Store["commit"]) { 159 | let commit = rootCommitCache.get(originalCommit) 160 | // console.log(">> to-rootCommit", commit ? "FROM_CACHE" : "CREATE", rootOptions) 161 | if (!commit) { 162 | const origCall = (mutation: string, payload: any) => originalCommit(mutation, payload, { root: true }) 163 | commit = commitFromOptions({}, rootOptions, origCall) 164 | rootCommitCache.set(originalCommit, commit) 165 | } 166 | return commit 167 | } 168 | 169 | function commitFromOptions( 170 | result: any, 171 | options: StoreOrModuleOptions, 172 | originalCommitCall: (mutation: string, payload: any) => void, 173 | hierarchy: string[] = [] 174 | ): any { 175 | if (options.mutations) 176 | createDirectMutations(result, options.mutations, originalCommitCall, hierarchy) 177 | if (options.modules) { 178 | for (const moduleName of Object.keys(options.modules)) { 179 | const moduleOptions = options.modules[moduleName] 180 | if (moduleOptions.namespaced) 181 | result[moduleName] = commitFromOptions({}, moduleOptions, originalCommitCall, [...hierarchy, moduleName]) 182 | else 183 | commitFromOptions(result, moduleOptions, originalCommitCall, hierarchy) 184 | } 185 | } 186 | return result 187 | } 188 | 189 | function createDirectMutations( 190 | result: any, 191 | mutationsImpl: MutationsImpl, 192 | originalCommitCall: (mutation: string, payload: any) => void, 193 | hierarchy?: string[] 194 | ) { 195 | const prefix = !hierarchy || hierarchy.length === 0 ? "" : `${hierarchy.join("/")}/` 196 | for (const name of Object.keys(mutationsImpl)) 197 | result[name] = (payload: any) => originalCommitCall(`${prefix}${name}`, payload) 198 | } 199 | 200 | // Actions 201 | 202 | const dispatchCache = new WeakMap["dispatch"], any>() 203 | 204 | function toDirectDispatch(options: StoreOrModuleOptions, originalDispatch: Store["dispatch"]) { 205 | let dispatch = dispatchCache.get(originalDispatch) 206 | // console.log(">> to-dispatch", dispatch ? "FROM_CACHE" : "CREATE", options) 207 | if (!dispatch) { 208 | dispatch = dispatchFromOptions({}, options, originalDispatch) 209 | dispatchCache.set(originalDispatch, dispatch) 210 | } 211 | return dispatch 212 | } 213 | 214 | const rootDispatchCache = new WeakMap["dispatch"], any>() 215 | 216 | function toDirectRootDispatch(rootOptions: StoreOptions, originalDispatch: Store["dispatch"]) { 217 | let dispatch = rootDispatchCache.get(originalDispatch) 218 | // console.log(">> to-rootDispatch", dispatch ? "FROM_CACHE" : "CREATE", rootOptions) 219 | if (!dispatch) { 220 | const origCall = (mutation: string, payload: any) => originalDispatch(mutation, payload, { root: true }) 221 | dispatch = dispatchFromOptions({}, rootOptions, origCall) 222 | rootDispatchCache.set(originalDispatch, dispatch) 223 | } 224 | return dispatch 225 | } 226 | 227 | function dispatchFromOptions( 228 | result: any, 229 | options: StoreOrModuleOptions, 230 | originalDispatchCall: (action: string, payload: any) => any, 231 | hierarchy: string[] = [] 232 | ): any { 233 | if (options.actions) 234 | createDirectActions(result, options.actions, originalDispatchCall, hierarchy) 235 | if (options.modules) { 236 | for (const moduleName of Object.keys(options.modules)) { 237 | const moduleOptions = options.modules[moduleName] 238 | if (moduleOptions.namespaced) 239 | result[moduleName] = dispatchFromOptions({}, moduleOptions, originalDispatchCall, [...hierarchy, moduleName]) 240 | else 241 | dispatchFromOptions(result, moduleOptions, originalDispatchCall, hierarchy) 242 | } 243 | } 244 | return result 245 | } 246 | 247 | function createDirectActions( 248 | result: any, 249 | actionsImpl: ActionsImpl, 250 | originalDispatchCall: (action: string, payload: any) => any, 251 | hierarchy?: string[] 252 | ) { 253 | const prefix = !hierarchy || hierarchy.length === 0 ? "" : `${hierarchy.join("/")}/` 254 | for (const name of Object.keys(actionsImpl)) 255 | result[name] = (payload?: any) => originalDispatchCall(`${prefix}${name}`, payload) 256 | } 257 | 258 | // GetterContext 259 | 260 | const getterContextCache = new WeakMap() 261 | 262 | function getModuleGetterContext(args: [any, any, any, any], options: ModuleOptions, rootOptions?: StoreOptions) { 263 | const [state, getters, rootState, rootGetters] = args 264 | let context = actionContextCache.get(state) 265 | // console.log(">> to-getterContext", context ? "FROM_CACHE" : "CREATE", options) 266 | if (!context) { 267 | if (rootOptions) { 268 | context = { 269 | get rootState() { 270 | return rootState 271 | }, 272 | get rootGetters() { 273 | return toDirectGetters(rootOptions!, rootGetters) 274 | }, 275 | get state() { 276 | return state 277 | }, 278 | get getters() { 279 | return toDirectGetters(options, getters) 280 | } 281 | } 282 | } else { 283 | context = { 284 | get state() { 285 | return state 286 | }, 287 | get getters() { 288 | return toDirectGetters(options, getters) 289 | } 290 | } 291 | } 292 | if (state) // Can be undefined in unit tests 293 | getterContextCache.set(state, context) 294 | } 295 | 296 | return context 297 | } 298 | 299 | // ActionContext 300 | 301 | const actionContextCache = new WeakMap() 302 | 303 | function getModuleActionContext( 304 | originalContext: ActionContext, 305 | options: ModuleOptions, 306 | rootOptions?: StoreOptions 307 | ): any { 308 | let context = actionContextCache.get(originalContext.state) 309 | // console.log(">> to-actionContext", context ? "FROM_CACHE" : "CREATE", options) 310 | if (!context) { 311 | if (rootOptions) { 312 | context = { 313 | get rootState() { 314 | return originalContext.rootState 315 | }, 316 | get rootGetters() { 317 | return toDirectGetters(rootOptions!, originalContext.rootGetters) 318 | }, 319 | get rootCommit() { 320 | return toDirectRootCommit(rootOptions!, originalContext.commit) 321 | }, 322 | get rootDispatch() { 323 | return toDirectRootDispatch(rootOptions!, originalContext.dispatch) 324 | }, 325 | get state() { 326 | return originalContext.state 327 | }, 328 | get getters() { 329 | return toDirectGetters(options, originalContext.getters) 330 | }, 331 | get commit() { 332 | return toDirectCommit(options, originalContext.commit) 333 | }, 334 | get dispatch() { 335 | return toDirectDispatch(options, originalContext.dispatch) 336 | } 337 | } 338 | } else { 339 | context = { 340 | get state() { 341 | return originalContext.state 342 | }, 343 | get getters() { 344 | return toDirectGetters(options, originalContext.getters) 345 | }, 346 | get commit() { 347 | return toDirectCommit(options, originalContext.commit) 348 | }, 349 | get dispatch() { 350 | return toDirectDispatch(options, originalContext.dispatch) 351 | } 352 | } 353 | } 354 | if (originalContext.state) // Can be undefined in unit tests 355 | actionContextCache.set(originalContext.state, context) 356 | } 357 | return context 358 | } 359 | -------------------------------------------------------------------------------- /tests/action-context.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDirectStore, defineModule } from "../src/direct-vuex" 2 | 3 | describe("Action Contexts", () => { 4 | 5 | test("Use 'dispatch' and 'rootDispatch' from action implementation", async () => { 6 | const mod1 = defineModule({ 7 | namespaced: true, 8 | actions: { 9 | async a2(context: any, payload: { p2: number }) { 10 | const { dispatch, rootDispatch } = mod1ActionContext(context) 11 | 12 | const p3: number = await dispatch.a3({ p3: 123 }) 13 | expect(p3).toBe(123) 14 | 15 | const p3bis: number = await rootDispatch.mod1.a3({ p3: 123 }) 16 | expect(p3bis).toBe(123) 17 | }, 18 | async a3(context: any, payload: { p3: number }) { 19 | return payload.p3 20 | } 21 | } 22 | }) 23 | const mod1ActionContext = (context: any) => moduleActionContext(context, mod1) 24 | 25 | const { store, rootActionContext, moduleActionContext } = createDirectStore({ 26 | actions: { 27 | async a1(context: any, payload: { p1: string }) { 28 | const { dispatch, rootDispatch } = rootActionContext(context) 29 | 30 | expect(dispatch.a1).toBeDefined() 31 | expect(rootDispatch.a1).toBeDefined() 32 | 33 | return payload.p1 34 | } 35 | }, 36 | modules: { 37 | mod1 38 | } 39 | }) 40 | 41 | await store.dispatch.a1({ p1: "abc" }) 42 | await store.dispatch.mod1.a2({ p2: 123 }) 43 | }) 44 | }) -------------------------------------------------------------------------------- /tests/namespaced.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDirectStore } from "../src/direct-vuex" 2 | 3 | describe("Namespaced Modules", () => { 4 | 5 | test("Access to namespaced action", async () => { 6 | const { store } = createDirectStore({ 7 | actions: { 8 | a1: async (context, payload: { p1: string }) => payload.p1 9 | }, 10 | modules: { 11 | mod1: { 12 | namespaced: true, 13 | actions: { 14 | a2: async (context, payload: { p2: number }) => payload.p2 15 | } 16 | } 17 | } 18 | }) 19 | 20 | const p1: string = await store.dispatch.a1({ p1: "abc" }) 21 | expect(p1).toBe("abc") 22 | 23 | const p2: number = await store.dispatch.mod1.a2({ p2: 123 }) 24 | expect(p2).toBe(123) 25 | }) 26 | 27 | test("Access to namespaced mutation", async () => { 28 | const { store } = createDirectStore({ 29 | mutations: { 30 | mu1: (state, payload: { p1: string }) => { } 31 | }, 32 | modules: { 33 | mod1: { 34 | namespaced: true, 35 | mutations: { 36 | mu2: (state, payload: { p2: number }) => { } 37 | } 38 | } 39 | } 40 | }) 41 | 42 | store.commit.mu1({ p1: "abc" }) 43 | store.commit.mod1.mu2({ p2: 123 }) 44 | }) 45 | 46 | test("Access to namespaced getter", async () => { 47 | const { store } = createDirectStore({ 48 | getters: { 49 | g1: (state) => "abc" 50 | }, 51 | modules: { 52 | mod1: { 53 | namespaced: true, 54 | getters: { 55 | g2: (state: any) => 123 56 | } 57 | } 58 | } 59 | }) 60 | 61 | const g1: string = store.getters.g1 62 | expect(g1).toBe("abc") 63 | const g2: number = store.getters.mod1.g2 64 | expect(g2).toBe(123) 65 | }) 66 | 67 | test("Access to namespaced getter with parameter", async () => { 68 | const { store } = createDirectStore({ 69 | getters: { 70 | hello: state => (name: string) => `Hello, ${name}!` 71 | }, 72 | }) 73 | 74 | const g1: string = store.getters.hello("John") 75 | expect(g1).toBe("Hello, John!") 76 | }) 77 | }) -------------------------------------------------------------------------------- /tests/non-namespaced.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDirectStore } from "../src/direct-vuex" 2 | 3 | describe("Non-Namespaced Modules", () => { 4 | 5 | test("Merge module action in the root store", async () => { 6 | const { store } = createDirectStore({ 7 | actions: { 8 | a1: async (context: any, payload: { p1: string }) => payload.p1 9 | }, 10 | modules: { 11 | mod1: { 12 | namespaced: false, 13 | actions: { 14 | a2: async (context: any, payload: { p2: number }) => payload.p2 15 | } 16 | } 17 | } 18 | }) 19 | 20 | const p1: string = await store.dispatch.a1({ p1: "abc" }) 21 | expect(p1).toBe("abc") 22 | 23 | const p2: number = await store.dispatch.a2({ p2: 123 }) 24 | expect(p2).toBe(123) 25 | }) 26 | 27 | test("Merge module action: omit the 'namespaced' property", async () => { 28 | const { store } = createDirectStore({ 29 | actions: { 30 | a1: async (context: any, payload: { p1: string }) => payload.p1 31 | }, 32 | modules: { 33 | mod1: { 34 | actions: { 35 | a2: async (context: any, payload: { p2: number }) => payload.p2 36 | } 37 | } 38 | } 39 | }) 40 | 41 | const p1: string = await store.dispatch.a1({ p1: "abc" }) 42 | expect(p1).toBe("abc") 43 | 44 | const p2: number = await store.dispatch.a2({ p2: 123 }) 45 | expect(p2).toBe(123) 46 | }) 47 | 48 | test("Merge module mutation in the root store", async () => { 49 | const { store } = createDirectStore({ 50 | mutations: { 51 | mu1: (state: any, payload: { p1: string }) => { } 52 | }, 53 | modules: { 54 | mod1: { 55 | namespaced: false, 56 | mutations: { 57 | mu2: (state: any, payload: { p2: number }) => { } 58 | } 59 | } 60 | } 61 | }) 62 | 63 | store.commit.mu1({ p1: "abc" }) 64 | store.commit.mu2({ p2: 123 }) 65 | }) 66 | 67 | test("Merge module getter in the root store", async () => { 68 | const { store } = createDirectStore({ 69 | getters: { 70 | g1: (state: any) => "abc" 71 | }, 72 | modules: { 73 | mod1: { 74 | namespaced: false, 75 | getters: { 76 | g2: (state: any) => 123 77 | } 78 | } 79 | } 80 | }) 81 | 82 | const g1: string = store.getters.g1 83 | expect(g1).toBe("abc") 84 | const g2: number = store.getters.g2 85 | expect(g2).toBe(123) 86 | }) 87 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ES2015", 5 | "module": "ES2015", 6 | "sourceMap": false, 7 | "lib": ["es2020", "DOM"], 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "outDir": "build/compiled-esm", 11 | "noEmitOnError": true, 12 | "declaration": false 13 | }, 14 | "include": [ 15 | "src", 16 | "types" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "dist", 21 | "dist-esm" 22 | ] 23 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**" 9 | ] 10 | }, 11 | "rules": { 12 | "arrow-parens": false, 13 | "curly": false, 14 | "eofline": false, 15 | "indent": [ 16 | true, 17 | "spaces", 18 | 2 19 | ], 20 | "interface-name": false, 21 | "max-classes-per-file": false, 22 | "max-line-length": false, 23 | "member-access": [ 24 | true, 25 | "no-public" 26 | ], 27 | "no-conditional-assignment": false, 28 | "no-consecutive-blank-lines": false, 29 | "no-empty": [ 30 | true, 31 | "allow-empty-catch", 32 | "allow-empty-functions" 33 | ], 34 | "no-shadowed-variable": false, 35 | "no-string-literal": false, 36 | "no-var-requires": false, 37 | "ordered-imports": true, 38 | "object-literal-sort-keys": false, 39 | "object-literal-key-quotes": false, 40 | "only-arrow-functions": false, 41 | "prefer-const": true, 42 | "quotemark": [ 43 | true, 44 | "double" 45 | ], 46 | "semicolon": [ 47 | true, 48 | "never" 49 | ], 50 | "space-before-function-paren": [ 51 | true, 52 | { 53 | "anonymous": "always", 54 | "named": "never", 55 | "asyncArrow": "always" 56 | } 57 | ], 58 | "trailing-comma": [ 59 | true, 60 | { 61 | "singleline": "never" 62 | } 63 | ], 64 | "variable-name": [ 65 | true, 66 | "allow-leading-underscore", 67 | "allow-pascal-case" 68 | ] 69 | } 70 | } -------------------------------------------------------------------------------- /types/direct-types.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionContext, Store } from "vuex" 2 | import { ActionsImpl, GettersImpl, ModuleOptions, ModulesImpl, MutationsImpl, StoreOptions, StoreOrModuleOptions } from "./index" 3 | 4 | export interface CreatedStore { 5 | store: ToDirectStore 6 | 7 | rootGetterContext(args: [any, any]): DirectGetterContext 8 | moduleGetterContext( 9 | args: [any, any, any, any], module: O 10 | ): DirectGetterContext 11 | 12 | rootActionContext(originalContext: ActionContext): DirectActionContext 13 | moduleActionContext( 14 | originalContext: ActionContext, 15 | module: O 16 | ): DirectActionContext 17 | } 18 | 19 | export type ToDirectStore = ShowContent<{ 20 | readonly state: ShowContent> 21 | getters: ShowContent> 22 | commit: ShowContent> 23 | dispatch: ShowContent> 24 | original: VuexStore 25 | }> 26 | 27 | export type VuexStore = Store>> & { 28 | direct: ToDirectStore 29 | } 30 | 31 | // State 32 | 33 | type DirectState = 34 | ToStateObj 35 | & GetStateInModules> 36 | 37 | type GetStateInModules = { 38 | readonly [M in keyof I]: DirectState 39 | } 40 | 41 | type ToStateObj = T extends (() => any) ? Readonly> : Readonly 42 | 43 | // Getters 44 | 45 | // export type SelfGetters = ToDirectGetters> 46 | 47 | type DirectGetters = 48 | ToDirectGetters> 49 | & GetGettersInModules>> 50 | & MergeGettersFromModules>> 51 | 52 | type GetGettersInModules = { 53 | readonly [M in keyof I]: DirectGetters 54 | } 55 | 56 | type ToDirectGetters = { 57 | readonly [K in keyof T]: ReadonlyReturnTypeExceptCb 58 | } 59 | 60 | type ReadonlyReturnTypeExceptCb any> = 61 | T extends ((...args1: any) => (...args2: any) => any) 62 | ? ReturnType 63 | : Readonly> 64 | 65 | type MergeGettersFromModules = 66 | UnionToIntersection>> 67 | 68 | // Mutations 69 | 70 | type DirectMutations = 71 | ToDirectMutations> 72 | & GetMutationsInModules>> 73 | & MergeMutationsFromModules>> 74 | 75 | type GetMutationsInModules = { 76 | [M in keyof I]: DirectMutations 77 | } 78 | 79 | type ToDirectMutations = { 80 | [K in keyof T]: Parameters[1] extends undefined 81 | ? (() => void) 82 | : (Extract[1], undefined> extends never ? 83 | ((payload: Parameters[1]) => void) : 84 | ((payload?: Parameters[1]) => void)) 85 | } 86 | 87 | type MergeMutationsFromModules = 88 | UnionToIntersection>> 89 | 90 | // Actions 91 | 92 | type DirectActions = 93 | ToDirectActions> 94 | & GetActionsInModules>> 95 | & MergeActionsFromModules>> 96 | 97 | type GetActionsInModules = { 98 | [M in keyof I]: DirectActions 99 | } 100 | 101 | type ToDirectActions = { 102 | [K in keyof T]: Parameters[1] extends undefined 103 | ? (() => PromiseOf>) 104 | : (Extract[1], undefined> extends never ? 105 | ((payload: Parameters[1]) => PromiseOf>) : 106 | ((payload?: Parameters[1]) => PromiseOf>)) 107 | } 108 | 109 | type MergeActionsFromModules = 110 | UnionToIntersection>> 111 | 112 | // ActionContext 113 | 114 | export type DirectActionContext = ShowContent<{ 115 | rootState: DirectState 116 | rootGetters: DirectGetters 117 | rootCommit: DirectMutations 118 | rootDispatch: DirectActions 119 | state: DirectState 120 | getters: DirectGetters 121 | commit: DirectMutations 122 | dispatch: DirectActions 123 | }> 124 | 125 | export type DirectGetterContext = ShowContent<{ 126 | rootState: DirectState 127 | rootGetters: DirectGetters 128 | state: DirectState 129 | getters: DirectGetters 130 | }> 131 | 132 | // Common helpers 133 | 134 | type PromiseOf = T extends Promise ? T : Promise 135 | 136 | type FilterNamespaced = Pick> 137 | type KeyOfType = { [P in keyof T]: T[P] extends U ? P : never }[keyof T] 138 | 139 | type FilterNotNamespaced = Pick> 140 | type NotKeyOfType = { [P in keyof T]: T[P] extends U ? never : P }[keyof T] 141 | 142 | type OrEmpty = T extends {} ? T : {} 143 | 144 | type UnionToIntersection = 145 | (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never 146 | 147 | type ShowContent = 148 | T extends Function ? T : 149 | T extends object ? 150 | T extends infer O ? { [K in keyof O]: ShowContentDepth1 } : never 151 | : T 152 | 153 | type ShowContentDepth1 = 154 | T extends Function ? T : 155 | T extends object ? 156 | T extends infer O ? { [K in keyof O]: O[K] } : never 157 | : T 158 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ActionContext } from "vuex" 2 | import { CreatedStore, DirectActionContext, DirectGetterContext } from "./direct-types" 3 | 4 | export function createDirectStore< 5 | O extends WithOptionalState, 6 | S = StateOf 7 | >(options: O & StoreOptions): CreatedStore 8 | 9 | export function defineModule< 10 | O extends WithOptionalState, 11 | S = StateOf 12 | >(options: O & ModuleOptions): O 13 | 14 | export function defineModules(): ((modules: T & ModulesImpl) => T) 15 | export function defineGetters(): ((getters: T & GettersImpl) => T) 16 | export function defineMutations(): ((mutations: T & MutationsImpl) => T) 17 | export function defineActions(actions: T & ActionsImpl): T 18 | 19 | export function localGetterContext( 20 | args: [any, any, ...any[]], options: O 21 | ): DirectGetterContext 22 | 23 | export function localActionContext( 24 | originalContext: ActionContext, 25 | options: O 26 | ): DirectActionContext 27 | 28 | /** 29 | * @deprecated Use `defineModule`. 30 | */ 31 | export function createModule< 32 | O extends WithOptionalState, 33 | S = StateOf 34 | >(options: O & ModuleOptions): O 35 | 36 | /** 37 | * @deprecated Use `defineModules`. 38 | */ 39 | export function createModules(): ((modules: T & ModulesImpl) => T) 40 | /** 41 | * @deprecated Use `defineGetters`. 42 | */ 43 | export function createGetters(): ((getters: T & GettersImpl) => T) 44 | /** 45 | * @deprecated Use `defineMutations`. 46 | */ 47 | export function createMutations(): ((mutations: T & MutationsImpl) => T) 48 | /** 49 | * @deprecated Use `defineActions`. 50 | */ 51 | export function createActions(actions: T & ActionsImpl): T 52 | 53 | 54 | export type WithOptionalState = { state: StateDeclaration } | {} 55 | export type StateOf = O extends { state: StateDeclaration } ? O["state"] : {} 56 | type StateDeclaration = any | (() => any) 57 | 58 | /* 59 | * Types for Vuex Store Options 60 | */ 61 | 62 | export interface StoreOrModuleOptions { 63 | state?: S extends object ? ((() => S) | S) : never, 64 | getters?: GettersImpl 65 | mutations?: MutationsImpl 66 | actions?: ActionsImpl 67 | modules?: ModulesImpl 68 | plugins?: PluginImpl[] 69 | } 70 | 71 | export interface StoreOptions extends StoreOrModuleOptions { 72 | strict?: boolean 73 | } 74 | 75 | export interface ModuleOptions extends StoreOrModuleOptions { 76 | namespaced?: boolean 77 | } 78 | 79 | export interface ModulesImpl { [moduleName: string]: ModuleOptions } 80 | 81 | export interface GettersImpl { 82 | [name: string]: GetterImpl 83 | } 84 | 85 | export type GetterImpl = (state: S, getters: G, rootState: any, rootGetters: any) => any 86 | 87 | export interface MutationsImpl { 88 | [name: string]: MutationImpl 89 | } 90 | 91 | export type MutationImpl = (state: S, payload: any) => void 92 | 93 | export interface ActionsImpl { 94 | [name: string]: ActionImpl 95 | } 96 | 97 | export type ActionImpl = (context: ActionContext, payload: any) => any 98 | 99 | export type PluginImpl = (store: any) => any --------------------------------------------------------------------------------