vuex icon indicating copy to clipboard operation
vuex copied to clipboard

feat(types): support experimental stricter types

Open Char2sGu opened this issue 3 years ago • 9 comments

This is a fully compatible type-only feature which is able to greatly improve the development experience!

It added a type-only option stricterTypes:

const store = createStore(
  {
    stricterTypes: true, // <--- set the type-only option `stricterTypes` to `true`
    // ...
  },
);

Then, the store object's type will be StricterStore<...>, which provides a lot of exciting type features:

  • store.state now contains the state of all the modules
    store.state.module1.module2.deepState;
    
  • store.getters is now strictly typed, it not only contains the types of the root getters but also the ones in namespaced (namespaced: true) modules
    store.getters["module1/module2/deepGetter"]; // auto-completion supported
    
  • store.commit() and store.dispatch() are super intelligent now, the first paremeter type supports auto-completion of the paths to both the root mutations/actions and the ones in namespaced modules, and the type of the second parameter payload will be the same as the type of the second parameter of the target mutation/action. Whatsmore, the return type of store.dispath will be the same as the return type of the target action. Ofcourse it supports both (type, payload, options) and (payloadWithType, options)
const store = createStore(
  {
    stricterTypes: true,
    state: { state1: 1 },
    getters: { getter1: () => 1 },
    mutations: { mutation1: (state, payload: { a: string }) => {} },
    actions: { action1: async (context, payload: { a: string }) => 1 },
    modules: {
      module1: {
        namespaced: true,
        state: { state2: "" },
        getters: { getter2: () => "" },
        mutations: { mutation2: (state, payload: { b: number }) => {} },
        actions: { action2: async (context, payload: { b: number }) => "" },
        modules: {
          module2: {
            namespaced: false,
            state: { state3: true },
            getters: { getter3: () => true },
            mutations: { mutation3: (state, payload: { c: boolean }) => {} },
            actions: {
              action3: async (context, payload: { c: boolean }) => true,
            },
          },
        },
      },
    },
  }
);

state-1 state-2 state-3 getter-1 getter-2 mutation-types mutation-1 mutation-2 action-types action-1 action-2


Known Problem: The stricter types have great benefits in using the store, but will cause the state to be any when defining the store.

Char2sGu avatar Jul 28 '21 11:07 Char2sGu

好耶!

akirarika avatar Aug 23 '21 16:08 akirarika

Drive-by comment: thanks for making this! Scary to see that TypeScript metaprogramming goes this deep (path generation).

Hoping to help test this.

  • Would it be possible to make stricterTypes part of the options argument instead? It looks strange to add a new dummy argument.
  • Inferring state, this should also remove the need for typing State separately, right? That suggests https://github.com/vuejs/vuex/blob/4.0/docs/guide/typescript-support.md needs an update.

tommie avatar Aug 24 '21 09:08 tommie

Drive-by comment: thanks for making this! Scary to see that TypeScript metaprogramming goes this deep (path generation).

Hoping to help test this.

  • Would it be possible to make stricterTypes part of the options argument instead? It looks strange to add a new dummy argument.
  • Inferring state, this should also remove the need for typing State separately, right? That suggests https://github.com/vuejs/vuex/blob/4.0/docs/guide/typescript-support.md needs an update.

Tests are coming soon... :]

  • Yes, that's easy, I will implement it right now. :-)
  • I'm not very sure about the behavior of the stricter types in actually use, there may probably be much unknown problems, so I think it may be better to wait for the stricter types to get mature enough?

Char2sGu avatar Aug 24 '21 10:08 Char2sGu

I think it may be better to wait for the stricter types to get mature enough?

I agree with the sentiment, but I'm guessing the PR is more likely to be accepted if it includes a documentation update. Since this is an opt-in extension, I'd assume it would be about adding a new section rather than replacing the existing. But I'm not a maintainer, so I understand if you'd want to wait for a review.

tommie avatar Aug 24 '21 10:08 tommie

LGTM, it's a long term problem.

wjq990112 avatar Aug 24 '21 10:08 wjq990112

Some thoughts after converting a PoC:

  • Using type Store = typeof store and InjectionKey<Store> makes more sense now. Sadly that doesn't have a nice fallback for when stricter types are disabled (since the old State interface isn't used). The State then becomes unknown and type checking in mutations fails. I wonder if there's a middle-ground that could unify them during a transition.

  • Needs

    export function useStore<S extends StricterStore<any>>(injectKey: InjectionKey<S>, stricterTypes: true): S;
    
  • It would be nice to be able to avoid stricterTypes in useStore, of course.

  • I use a pattern of giving the store reference and a mutation name as a string to some constructors. This fails because the union type of mutation string literals can't take a string. The type extraction could probably be made more readable: private store: Store, private name: Parameters<typeof store.commit>[0]

tommie avatar Aug 24 '21 21:08 tommie

Some thoughts after converting a PoC:

  • Using type Store = typeof store and InjectionKey<Store> makes more sense now. Sadly that doesn't have a nice fallback for when stricter types are disabled (since the old State interface isn't used). The State then becomes unknown and type checking in mutations fails. I wonder if there's a middle-ground that could unify them during a transition.
  • Needs
    export function useStore<S extends StricterStore<any>>(injectKey: InjectionKey<S>, stricterTypes: true): S;
    
  • It would be nice to be able to avoid stricterTypes in useStore, of course.
  • I use a pattern of giving the store reference and a mutation name as a string to some constructors. This fails because the union type of mutation string literals can't take a string. The type extraction could probably be made more readable: private store: Store, private name: Parameters<typeof store.commit>[0]

Thanks~❤

  • createStore() now accepts the virtual parameter stricterTypes in its options:
    createStore({
      stricterTypes: true;
      // ...
    })
    
  • useStore() now supports both Store and StricterStore without any extra parameters:
    const key: InjectionKey<typeof store> = Symbol();
    const store_ = useStore(key); // type: StricterStore<...>
    
  • Sorry, I didn't understand your case well... :[
    If you mean to have the literal string type of mutations, the type CommitType can be used, but to extract the literal string type of the mutations directly from a store, you may have to create a tool type:
    type ExtractCommitType<
      Store extends StricterStore<any, any, any, any, any>
    > = Store extends StricterStore<
      infer RootState,
      any,
      infer Mutations,
      any,
      infer Modules
    >
      ? CommitType<RootState, Mutations, Modules>
      : never;
    

Char2sGu avatar Aug 25 '21 01:08 Char2sGu

Thanks for the fixes!

createStore() now accepts the virtual parameter stricterTypes in its options:

I needed to move the stricterTypes overload first (just like with useStore) for type inference to work. Without it, the state argument to mutator functions became unknown.

type ExtractCommitType

Yepp, I think you understood, and that's the problem I wanted to solve:

function doit(store: Store, name: ExtractCommitType<Store>) {
  store.commit(name);
}

Assuming other people have been doing the same thing, I think this would be useful to add as part of the API. (Same with actions.)

Given that TypeScript has ReturnType<> and Parameter<> (as opposed to ExtractReturnType<>), I don't think the name is great, but that's a tiny thing.

tommie avatar Aug 25 '21 08:08 tommie

Thanks for the fixes!

createStore() now accepts the virtual parameter stricterTypes in its options:

I needed to move the stricterTypes overload first (just like with useStore) for type inference to work. Without it, the state argument to mutator functions became unknown.

type ExtractCommitType

Yepp, I think you understood, and that's the problem I wanted to solve:

function doit(store: Store, name: ExtractCommitType<Store>) {
  store.commit(name);
}

Assuming other people have been doing the same thing, I think this would be useful to add as part of the API. (Same with actions.)

Given that TypeScript has ReturnType<> and Parameter<> (as opposed to ExtractReturnType<>), I don't think the name is great, but that's a tiny thing.

Yes, that's a known problem, I haven't found a way to make the type inference work well in the options of createStore()... T_T

I just realized that this kind of naming style does have some problems, there are also some built-in tool types in TypeScript are named like Extract, Exclude, Pick and Omit, maybe it would be better to use StoreCommitType.

It's a good idea to add tool types like this to the API. Although tool types like this are quite easy to implement, a large number of TypeScript users may still not be able to implement them. More tool types will be available soon.🎉

By the way, is there a code format style specified in this repo? I didn't found a config like prettier.config.js here, and formatting the code with prettier or directly using VSC's built-in formatter will both cause a lot of changes. I had to format only part of the code to avoid conflicts. :[

Char2sGu avatar Aug 25 '21 10:08 Char2sGu