-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Write Stores in Typescript #564
Comments
/ping @ktsn |
Thank you for trying Vuex with TypeScript. // Declare each action type
type ActionA = {
type: 'A',
value: number
}
type ActionB = {
type: 'B',
value: string
}
// Declare the union type of actions
type Action = ActionA | ActionB
// Provide action type
store.dispatch<Action>({
type: 'A',
value: 1
}) About defining the store as class, I believe we can make such binding like vue-class-component and I personally interested in it. Also, there is an experiment for type safety of Vuex. https://github.com/HerringtonDarkholme/kilimanjaro |
Mh, okay, this might be a start. For better clarification, I'll provide a simplified example for a store that will later get included as a module:
export class State {
// persons above 24 years old
adults: number = 2
// persons between 16 and 24 years old
juveniles: number = 0
// persons below 16 years old
children: number = 0
}
import { State } from './state'
export type personIdentifier = 'adults' | 'juveniles' | 'children'
export class Mutations {
// increment the chosen persons type by one
inc (state: State, key: personIdentifier) {
state[key]++
}
// decrement the chosen persons type by one
dec (state: State, key: personIdentifier) {
state[key]--
}
}
import { Store } from 'vuex'
import { State } from './state'
export class Actions {
inc ({ commit }: Store<State>) {
// ??????
}
} skipping the getters for now, since they're unimportant. This is where I am currently, and I would combine these classes into a Is there a solution how I might write these actions as type safe methods? Your example is not clear enough for me to apply the pattern you provided, I think. |
Hm, there maybe no solution when |
small heads-up: it was really straightforward to implement a Store-Module as a standalone npm package in typescript, with tooling/tests/typings and namespaced:true, and then use it dynamically within the core app. Thanks to the recent efforts of you guys for the typescript support, implementing the interfaces really helped to get it right immediately! In my opinion the only puzzle-piece left is typesafe commit/dispatch (with namespaced), but I think this is a hard problem to bolt-on vuex as-is. One idea would be to generate types on the "new Vuex.Store" call in the main code, which map the generated identifier-string to the underlying function or sth like that. But ideally, there could be an alternate way to call commit/dispatch, maybe through wrapper classes to Module that does the actual calling behind the scenes or something. This seems to be an architectural issue, which is really not easy to resolve. On the other hand I think it would really, really great to solve this. Even if approx. most vuex users do "just" use ES7, they could benefit from intellisense/hints from their Editors/IDEs greatly, providing huge ergonomic value once stores become nontrivial in size. |
@Anonyfox Would you mind creating an example repository or snippet for vuex with typescript, modules and classes as far as you got it so far? I am currently setting up a vue project which probably will contain a few store modules. I understand using vuex with ES6 but I am struggling with properly setting up my project with typescript. I would like to have everything structured well but it seems a bit complicated at the moment :) As for now I created two global files ( |
@morhi sure, I extracted a simple (but not trivial) store we're using. https://github.com/Anonyfox/vuex-store-module-example please respect that I can not give a valid license, this is just intended for demonstrating. /cc @ktsn |
I think it would be nice if we can use namespaced module like as /*
* equivalent to ctx.commit("foo/bar/action", arg)
*/
ctx.commit.foo.bar("action", arg);
// OR
ctx.foo.bar.commit("action", arg);
// OR
ctx.modules.foo.bar.commit("action", arg);
/*
* equivalent to ctx.getters("foo/bar/prop")
*/
ctx.getters.foo.bar.prop
// OR
ctx.foo.bar.getters.prop
// OR
ctx.modules.foo.bar.getters.prop This will make adding and combining types more easy. |
@Anonyfox wow! thank you very much! I adopted your example into my environment and got it working 👍 |
Related: #532 One way to ensure type consnistency between dispatch/commit payload vs. action/mutation handler, is to declare both of them at once. Eg. However, you'd need some sort of runtime decoration approach to setup necessary pre-initializations prior to app running. Also, Typescript doesn't seem to complain if I override a method with a different payload type parameter form, which is something that i'd need to enforce, though (anyway to do this?). Overall, setting up all the decorators and custom initilizations at runtime is a lot of work. |
FYI, I wrote helper to make store half-decent type safe. It seems to work well except namespaced module. |
@wonderful-panda Is it possible with your code to add modules within modules recursively? |
@Glidias yes. Nested module example is below: const module1 = builder.createModule<...>({ /* module1 definition */ });
const module2 = builder.createModule<...>({ /* module2 definition */ });
const parentModule =
builder.addModule("bar", module1)
.addModule("baz", module2) // { modules: { bar: module1, baz: module2 } }
.createModule<...>({ /* parentModule definition without `modules` */ });
const store =
builder.addModule("foo", parentModule)
.createStore<...>({ /* store options without `modules` */ }); |
@wonderful-panda See the comments in the gist for continued discussion in that area. Regarding dynamic namespacing of modules + strict typing, one idea i was considering is that after a module tree is initialized, you traverse through it's state tree and will always set a For strictly typed dispatches of module-specific mutations/actions, one way is to adopt a generic helper wrapper method to commit something through a given context (with/without namespace prefixing..) https://github.com/Glidias/vuex-store-module-example/blob/master/src/util/vuexhelpers.ts However, this will result in a bit of additional performance overhead of calling the wrapper helper function (remember, there's no inlining in Typescript). But i guess, it's okay and shouldn't be too much of an issue.
Also, if your module needs to respond to both namespaced vs non-namespaced mutations/actions, |
I've done some work towards this end and the solution is coming along nicely. I've created a set of decorators similar to vue-class-component, which combine with a few conventions to make a pretty nice experience in TS. It's currently light on documentation (like, none heh) but the tests tell the story pretty well, I hope... https://github.com/snaptopixel/vuex-ts-decorators/blob/master/test/index.ts I've invited @ktsn and @yyx990803 as collaborators but happy to receive ideas and pull requests from anyone. I'll be writing some docs and examples asap. |
Thank you very much for all of your works! |
Nice @ktsn I'll take a look at your work, thanks for the heads up! Give mine a look too as you find time. |
@snaptopixel I just tried out your decorators and I think they are pretty great! Very simple to write and use. Defining stores isn't too different from normal Vuex and using them is exactly the same as normal Vuex except the added type safety (including with commit and dispatch!) which is nice! However there are a few things that I could not get to work, so I added some questions/issues to your repository. |
Awesome @chanon thank you so much for trying them out and filing issues. That helps tremendously. I'm planning on adding a proper readme soon and will definitely be working on the issues posted. |
My team was looking for a simple, tiny and unobtrusive solution. After several iterations I distilled it to this: https://github.com/istrib/vuex-typescript. //Vuex Module (like in JS + type annotations, no classes):
export const basket = {
namespaced: true,
mutations: {
appendItem(state: BasketState, item: { product: Product; atTheEnd: boolean }) {
state.items.push({ product: item.product, isSelected: false });
},
...
//Strongly-typed wrappers:
const { commit } =
getStoreAccessors<BasketState, RootState>("basket"); // Pass namespace here, if we make the module namespaced: true.
// commit is a higher-order function which gets handler function as argument
// and returns a strongly-typed "accessor" function which internally calls the standard store.commit() method.
// Implementation of commit is trivial: https://github.com/istrib/vuex-typescript/blob/master/src/index.ts#L103
// you get intellisense here:
export const commitAppendItem = commit(basket.mutations.appendItem);
// commitAppendItem is a function with strongly-typed signature
// so you get intellisense for function name and types of its arguments here too:
import * as basket from "./store/basket";
basket.commitAppendItem(this.$store, newItem); |
@istrib I really like what you've done in vuex-typescript. I just wanted to take it a bit further—if you don't agree these ideas, that's totally okay. If you do agree, I'd be keen to incorporate these changes into vuex-typescript somehow. My main changes are:
I also took the point of view that we don't need to start with a vuex-store options object. If we treat the accessor-creator as a builder, then the store can be generated: import { getStoreBuilder } from "vuex-typex"
import Vuex, { Store, ActionContext } from "vuex"
import Vue from "vue"
const delay = (duration: number) => new Promise((c, e) => setTimeout(c, duration))
Vue.use(Vuex)
export interface RootState { basket: BasketState }
export interface BasketState { items: Item[] }
export interface Item { id: string, name: string }
const storeBuilder = getStoreBuilder<RootState>()
const moduleBuilder = storeBuilder.module<BasketState>("basket", { items: [] })
namespace basket
{
const appendItemMutation = (state: BasketState, payload: { item: Item }) => state.items.push(payload.item)
const delayedAppendAction = async (context: ActionContext<BasketState, RootState>) =>
{
await delay(1000)
basket.commitAppendItem({ item: { id: "abc123", name: "ABC Item" } })
}
export const commitAppendItem = moduleBuilder.commit(appendItemMutation)
export const dispatchDelayedAppend = moduleBuilder.dispatch(delayedAppendAction)
}
export default basket
/// in the main app file
const storeBuilder = getStoreBuilder<RootState>()
new Vue({
el: '#app',
template: "....",
store: storeBuilder.vuexStore()
})
|
What would be the recommended approach here? We are starting a new project at work and are really keen to have type safety on our stores (including commit/dispatch). |
I like your approach, @mrcrowl With https://github.com/istrib/vuex-typescript I searched for a solution which produces code that is identical to vanilla Vuex + a bit of simple stuff below it. For that reason I did not mind starting with Vuex options rather than have them built. That is also why I decided to explicitly pass $store into accessors. I like your variation for doing that much while still being tiny. https://github.com/istrib/vuex-typescript now contains your two great suggestions: making promises returned from dispatch strongly-typed and using function overloads for mutations/actions without payload. |
@istrib do you have gist example for reference please? |
Thanks @istrib for the starting point. I am looking to get Vuex setup with typescript and I just stumbled across I am doing: import * as Vue from 'vue';
import * as Vuex from 'vuex';
// vuex store
import {createStore} from './store';
Vue.use(Vuex);
new Vue({
el: '#app-main',
store: createStore()
}); But I am getting the error: [vuex] must call Vue.use(Vuex) before creating a store instance My directory structure is pretty close to the example: |
I've done an impl. of the two approach in a Todo app. Here's the repo Vue.js + Vuex (typescript & typex) • TodoMVC @michaelharrisonroth You're certainly doing export const createStore = new Vuex.Store<State>({ expect of `export const createStore = () => new Vuex.Store<State>({` in your store.ts file |
@DevoidCoding That was the issue and your TodoMVC is extremely helpful, thank you! |
So I made a small library wich allows you to get fully typed store in actions and components. |
For me adding the store state type in module declaration is enough to have auto-completion declare module "vue/types/vue" {
interface Vue {
$store: Store<RootState| any>;
}
} A more complete exemple : import Vue from "vue"
import Vuex, { StoreOptions} from "vuex"
import App from './App.vue'
Vue.use(Vuex)
interface RootState {
user: App.Models.User | null;
notifications: App.Models.Notification[];
}
const store: StoreOptions<RootState> = {
state: {
user: null,
notifications: []
},
mutations: {
login(state, user: User) {
state.user = user
},
logout(state) {
state.user = null
}
},
actions: {
},
modules: {
}
}
store = new Vuex.Store<RootState>(store);
declare module "vue/types/vue" {
interface Vue {
// https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#use-union-types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
$store: Store<RootState| any>;
}
}
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app') |
The |
Vuex has created a lot of pain for Typescript users. Now that we have the Composition API... do we even really need Vuex? In 2020, if I'm training a team to use Vue 3, why would I teach them Vuex? Creating stateful singletons that you can compose is a really nice pattern. Code is so much cleaner without Vuex. The arguments in favor of Vuex (in my mind) are:
What am I missing? |
Agree, I've trained my teammate to use Vue's injection to provide shared data between components. |
This is true, but Vue dev tools integration is not a small thing. It can be extremely useful for debugging. |
I'm kinda facing the same issue here, this is how my
But I get this warning:
Any idea how to fix this? |
A shout out to @paleo, direct-vuex is fantastic. Strongly typed coding within the store and (perhaps more importantly) when components access the store state and actions. I tried Vuex 4 today, but it didn't really do much for me (I'm still on Vue2 + Composition add-on). Is there an outline of how Vuex 4 will work for Typescript users? All I could find was an old roadmap with generic statements about "supporting Typescript" |
@rfox12 As far as I know, you'll need to wait for Vuex 5. Vuex 4 was focused on preparing for Vue 3 and allowing passing your own interface to Store<> Before Vuex 4, you'd have to do:
thus this.$store.state wouldn't be typed. Working around that (passing your own store interface instead of any) required some trickery |
I'll add two additional compelling reasons to keep Vuex in Composition API world:
So in short... I'm keeping Vuex for large projects, but I have an question. First some background. To get typescript support today I find it best to |
@rfox12 You should always use The difference with direct import is that when you use If you're not doing SSR, it's fine to directly import the store since it will be used by only one client (in browser). |
@Teebo Here is my solution: https://github.com/MikeMitterer/vue-ts-starter/tree/master/src/store
|
@MikeMitterer thank you so much, I will go through the repo to understand the setup, but from a glance, it looks good, thank you! |
@MikeMitterer, in the file https://github.com/MikeMitterer/vue-ts-starter/blob/master/src/store/utils/index.ts I see that in the actions return promises in the counter store, is the a requirement from |
Been subscribed to this issue for like 2 years, the absolute state of webdev. Glad I dropped this stuff, yikes. |
It is v4.0.0-rc.2 now. |
@ChanningHan I've been working on full type support for vuex in my personal time, and with TS 4.1 it seems definitely possible. The only thing holding it back is a problem with TS where it obnoxiously warns about "potentially infinite blah blah". I will be updating this issue: #1831 |
@ClickerMonkey Bravo~It will be exciting! |
Spent several hours trying to modularize vuex store with TypeScript, I feel TS is just a way to write your code two times more. I do understand the importance of type safety, but I gave up trying to describe all types... |
TypeScript works best when you just describe a few core domain types & the rest of your code has types inferred from how you operated on those core types & library types. It's especially important for libraries/frameworks to export APIs with types; a few |
How should you write Doing |
You can't. Its the same as in Vue 2. The newest version of Vuex is mainly for Vue 3 support. Wait for Vuex 5 or use vuex-module-decorators (or similar pkgs). There's also an issue in this repo that proposes using TS 4.1 template literal types for typing |
@3nuc I'm new to TypeScript so don't know what any of that means lol. As a workaround I'll be sticking to mapGetters as TS doesn't complain about this. State injection seems to working, but using helpers defeats the point of TS I think. Will take a look at |
https://github.com/posva/pinia - looks pretty good. Works with TypeScript out of the box. |
@usernamehw Yeah, I also using Pinia as TS replacement. :) |
People who prefer classical OOP-based concepts and not too functional looking code, can check out my library |
I couldn't find anything on google, so is this conveniently possible? Basically the bummer for me is that I have to call
dispatch
/commit
with a given identifier string to trigger something.My vision is to define stores as Typescript classes with typed methods (getting intellisense in editor and so on) which I can call instead of
dispatch("my_action", data)
and having to look up each and everything and check for errors manually all the time.Basically my problem is that my team is building a fairly large Vue/Vuex frontend for a core product of our company, consisting of already 18 fairly complex Stores as modules, and this year stuff is going to at least quadruple in size and complexity. We already decided to go on with typescript instead of ES7 and are in the process of migrating our backend/frontend code, but this thing really feels like a bummer.
I think this is not the typical "small" use case for Vue.js, since we're building a huge enterprise frontend with loads of requirements and edge cases, but isn't vuex(/flux) supposed to scale up when complexity rises?
Has anyone experience in building complex, type safe Vuex 2.0 stores in Typescript yet? Any help here would be appreciated
The text was updated successfully, but these errors were encountered: