]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat(store): pass state to getters as first argument
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 3 May 2021 08:28:42 +0000 (10:28 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 3 May 2021 09:01:17 +0000 (11:01 +0200)
BREAKING CHANGE: getters now receive the state as their first argument and it's properly typed so you can write getters with arrow functions:

```js
defineStore({
  state: () => ({ n: 0 }),
  getters: {
    double: state => state.n * 2
  }
})
```

To access other getters, you must still use the syntax that uses `this` **but it is now necessary to explicitely type the getter return type**. The same limitation exists in Vue for computed properties and it's a known limitation in TypeScript:

```ts
defineStore({
  state: () => ({ n: 0 }),
  getters: {
    double: state => state.n * 2,
    // the `: number` is necessary when accessing `this` inside of
    // a getter
    doublePlusOne(state): number {
      return this.double + 1
    },
  }
})
```

For more information, refer to [the updated documentation for getters](https://pinia.esm.dev/core-concepts/getters.html).

.eslintrc.js
src/mapHelpers.ts
src/store.ts
src/types.ts
test-dts/customizations.test-d.ts
test-dts/deprecated.test-d.ts
test-dts/mapHelpers.test-d.ts
test-dts/plugins.test-d.ts [new file with mode: 0644]
test-dts/store.test-d.ts

index a1de949955da6b002bfe0b6d4b51fd0f1c4b4b3b..e2eefaff489ff2ff65c2f9062dd2463372d0e877 100644 (file)
@@ -17,5 +17,6 @@ module.exports = {
     '@typescript-eslint/no-namespace': 'off',
     '@typescript-eslint/ban-ts-comment': 'off',
     '@typescript-eslint/ban-types': 'off',
+    '@typescript-eslint/no-empty-interface': 'off',
   },
 }
index b76d72eaabd7797dc3ae1424d59c9a1e0cb2076c..72b8f27e040ef8c0d2df60733e7a8941c445f1a7 100644 (file)
@@ -1,13 +1,15 @@
 import type Vue from 'vue'
 import {
   GenericStore,
-  GenericStoreDefinition,
+  GettersTree,
   Method,
   StateTree,
   Store,
   StoreDefinition,
 } from './types'
 
+type ComponentPublicInstance = Vue
+
 /**
  * Interface to allow customizing map helpers. Extend this interface with the
  * following properties:
@@ -49,10 +51,13 @@ type Spread<A extends readonly any[]> = A extends [infer L, ...infer R]
 function getCachedStore<
   Id extends string = string,
   S extends StateTree = StateTree,
-  G = Record<string, Method>,
+  G extends GettersTree<S> = GettersTree<S>,
   A = Record<string, Method>
->(vm: Vue, useStore: StoreDefinition<Id, S, G, A>): Store<Id, S, G, A> {
-  const cache = vm._pStores || (vm._pStores = {})
+>(
+  vm: ComponentPublicInstance,
+  useStore: StoreDefinition<Id, S, G, A>
+): Store<Id, S, G, A> {
+  const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {})
   const id = useStore.$id
   return (cache[id] || (cache[id] = useStore(vm.$pinia))) as Store<Id, S, G, A>
 }
@@ -96,9 +101,21 @@ export function setMapStoreSuffix(
  *
  * @param stores - list of stores to map to an object
  */
-export function mapStores<Stores extends GenericStoreDefinition[]>(
+export function mapStores<Stores extends any[]>(
   ...stores: [...Stores]
 ): Spread<Stores> {
+  if (__DEV__ && Array.isArray(stores[0])) {
+    console.warn(
+      `[🍍]: Directly pass all stores to "mapStores()" without putting them in an array:\n` +
+        `Replace\n` +
+        `\tmapStores([useAuthStore, useCartStore])\n` +
+        `with\n` +
+        `\tmapStores(useAuthStore, useCartStore)\n` +
+        `This will fail in production if not fixed.`
+    )
+    stores = stores[0]
+  }
+
   return stores.reduce((reduced, useStore) => {
     // @ts-ignore: $id is added by defineStore
     reduced[useStore.$id + mapStoreSuffix] = function (this: Vue) {
@@ -108,14 +125,14 @@ export function mapStores<Stores extends GenericStoreDefinition[]>(
   }, {} as Spread<Stores>)
 }
 
-type MapStateReturn<S extends StateTree, G> = {
+type MapStateReturn<S extends StateTree, G extends GettersTree<S>> = {
   [key in keyof S | keyof G]: () => Store<string, S, G, {}>[key]
 }
 
 type MapStateObjectReturn<
   Id extends string,
   S extends StateTree,
-  G,
+  G extends GettersTree<S>,
   A,
   T extends Record<
     string,
@@ -168,7 +185,7 @@ type MapStateObjectReturn<
 export function mapState<
   Id extends string,
   S extends StateTree,
-  G,
+  G extends GettersTree<S>,
   A,
   KeyMapper extends Record<
     string,
@@ -201,7 +218,12 @@ export function mapState<
  * @param useStore - store to map from
  * @param keys - array of state properties or getters
  */
-export function mapState<Id extends string, S extends StateTree, G, A>(
+export function mapState<
+  Id extends string,
+  S extends StateTree,
+  G extends GettersTree<S>,
+  A
+>(
   useStore: StoreDefinition<Id, S, G, A>,
   keys: Array<keyof S | keyof G>
 ): MapStateReturn<S, G>
@@ -216,7 +238,7 @@ export function mapState<Id extends string, S extends StateTree, G, A>(
 export function mapState<
   Id extends string,
   S extends StateTree,
-  G,
+  G extends GettersTree<S>,
   A,
   KeyMapper extends Record<
     string,
@@ -228,13 +250,13 @@ export function mapState<
 ): MapStateReturn<S, G> | MapStateObjectReturn<Id, S, G, A, KeyMapper> {
   return Array.isArray(keysOrMapper)
     ? keysOrMapper.reduce((reduced, key) => {
-        reduced[key] = function (this: Vue) {
+        reduced[key] = function (this: ComponentPublicInstance) {
           return getCachedStore(this, useStore)[key]
         } as () => any
         return reduced
       }, {} as MapStateReturn<S, G>)
     : Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => {
-        reduced[key] = function (this: Vue) {
+        reduced[key] = function (this: ComponentPublicInstance) {
           const store = getCachedStore(this, useStore)
           const storeKey = keysOrMapper[key]
           // for some reason TS is unable to infer the type of storeKey to be a
@@ -249,7 +271,7 @@ export function mapState<
 
 /**
  * Alias for `mapState()`. You should use `mapState()` instead.
- * @deprecated
+ * @deprecated use `mapState()` instead.
  */
 export const mapGetters = mapState
 
@@ -289,7 +311,7 @@ type MapActionsObjectReturn<A, T extends Record<string, keyof A>> = {
 export function mapActions<
   Id extends string,
   S extends StateTree,
-  G,
+  G extends GettersTree<S>,
   A,
   KeyMapper extends Record<string, keyof A>
 >(
@@ -319,7 +341,12 @@ export function mapActions<
  * @param useStore - store to map from
  * @param keys - array of action names to map
  */
-export function mapActions<Id extends string, S extends StateTree, G, A>(
+export function mapActions<
+  Id extends string,
+  S extends StateTree,
+  G extends GettersTree<S>,
+  A
+>(
   useStore: StoreDefinition<Id, S, G, A>,
   keys: Array<keyof A>
 ): MapActionsReturn<A>
@@ -334,7 +361,7 @@ export function mapActions<Id extends string, S extends StateTree, G, A>(
 export function mapActions<
   Id extends string,
   S extends StateTree,
-  G,
+  G extends GettersTree<S>,
   A,
   KeyMapper extends Record<string, keyof A>
 >(
@@ -343,13 +370,19 @@ export function mapActions<
 ): MapActionsReturn<A> | MapActionsObjectReturn<A, KeyMapper> {
   return Array.isArray(keysOrMapper)
     ? keysOrMapper.reduce((reduced, key) => {
-        reduced[key] = function (this: Vue, ...args: any[]) {
+        reduced[key] = function (
+          this: ComponentPublicInstance,
+          ...args: any[]
+        ) {
           return (getCachedStore(this, useStore)[key] as Method)(...args)
         } as Store<string, StateTree, {}, A>[keyof A]
         return reduced
       }, {} as MapActionsReturn<A>)
     : Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => {
-        reduced[key] = function (this: Vue, ...args: any[]) {
+        reduced[key] = function (
+          this: ComponentPublicInstance,
+          ...args: any[]
+        ) {
           return getCachedStore(this, useStore)[keysOrMapper[key]](...args)
         } as Store<string, StateTree, {}, A>[keyof KeyMapper[]]
         return reduced
@@ -384,7 +417,7 @@ type MapWritableStateObjectReturn<
 export function mapWritableState<
   Id extends string,
   S extends StateTree,
-  G,
+  G extends GettersTree<S>,
   A,
   KeyMapper extends Record<string, keyof S>
 >(
@@ -399,7 +432,12 @@ export function mapWritableState<
  * @param useStore - store to map from
  * @param keys - array of state properties
  */
-export function mapWritableState<Id extends string, S extends StateTree, G, A>(
+export function mapWritableState<
+  Id extends string,
+  S extends StateTree,
+  G extends GettersTree<S>,
+  A
+>(
   useStore: StoreDefinition<Id, S, G, A>,
   keys: Array<keyof S>
 ): MapWritableStateReturn<S>
@@ -414,7 +452,7 @@ export function mapWritableState<Id extends string, S extends StateTree, G, A>(
 export function mapWritableState<
   Id extends string,
   S extends StateTree,
-  G,
+  G extends GettersTree<S>,
   A,
   KeyMapper extends Record<string, keyof S>
 >(
@@ -423,11 +461,12 @@ export function mapWritableState<
 ): MapWritableStateReturn<S> | MapWritableStateObjectReturn<S, KeyMapper> {
   return Array.isArray(keysOrMapper)
     ? keysOrMapper.reduce((reduced, key) => {
+        // @ts-ignore
         reduced[key] = {
-          get(this: Vue) {
+          get(this: ComponentPublicInstance) {
             return getCachedStore(this, useStore)[key]
           },
-          set(this: Vue, value) {
+          set(this: ComponentPublicInstance, value) {
             // it's easier to type it here as any
             return (getCachedStore(this, useStore)[key] = value as any)
           },
@@ -437,10 +476,10 @@ export function mapWritableState<
     : Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => {
         // @ts-ignore
         reduced[key] = {
-          get(this: Vue) {
+          get(this: ComponentPublicInstance) {
             return getCachedStore(this, useStore)[keysOrMapper[key]]
           },
-          set(this: Vue, value) {
+          set(this: ComponentPublicInstance, value) {
             // it's easier to type it here as any
             return (getCachedStore(this, useStore)[
               keysOrMapper[key]
index 388c3a6553f6144531e602ebaac51de0b58a8a8c..5fc6b932868d3dd6c70ed4d2ac3645a0819bbb53 100644 (file)
@@ -21,6 +21,7 @@ import {
   StateDescriptor,
   PiniaCustomProperties,
   StoreDefinition,
+  GettersTree,
 } from './types'
 import { useStoreDevtools } from './devtools'
 import {
@@ -200,7 +201,7 @@ function initStore<Id extends string, S extends StateTree>(
 function buildStoreToUse<
   Id extends string,
   S extends StateTree,
-  G extends Record<string, Method>,
+  G extends GettersTree<S>,
   A extends Record<string, Method>
 >(
   partialStore: StoreWithState<Id, S>,
@@ -213,9 +214,11 @@ function buildStoreToUse<
 
   const computedGetters: StoreWithGetters<G> = {} as StoreWithGetters<G>
   for (const getterName in getters) {
+    // @ts-expect-error: it's only readonly for the users
     computedGetters[getterName] = computed(() => {
       setActivePinia(pinia)
       // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      // @ts-expect-error: the argument count is correct
       return getters[getterName].call(store, store)
     }) as StoreWithGetters<G>[typeof getterName]
   }
@@ -262,7 +265,7 @@ function buildStoreToUse<
 export function defineStore<
   Id extends string,
   S extends StateTree,
-  G /* extends Record<string, StoreGetterThis> */,
+  G extends GettersTree<S>,
   A /* extends Record<string, StoreAction> */
 >(options: {
   id: Id
@@ -310,7 +313,7 @@ export function defineStore<
         storeAndDescriptor[0],
         storeAndDescriptor[1],
         id,
-        getters as Record<string, Method> | undefined,
+        getters as GettersTree<S> | undefined,
         actions as Record<string, Method> | undefined
       )
 
@@ -321,7 +324,7 @@ export function defineStore<
       storeAndDescriptor[0],
       storeAndDescriptor[1],
       id,
-      getters as Record<string, Method> | undefined,
+      getters as GettersTree<S> | undefined,
       actions as Record<string, Method> | undefined
     )
   }
index 8ed832092fae7c09667642df0d74f3d02e545ca8..d0d9fa39bf736bb42ab671335c2412fdf57b2a82 100644 (file)
@@ -1,5 +1,8 @@
 import { Pinia } from './rootStore'
 
+/**
+ * Generic state of a Store
+ */
 export type StateTree = Record<string | number | symbol, any>
 
 /**
@@ -22,8 +25,6 @@ export function isPlainObject(
   )
 }
 
-export type NonNullObject = Record<any, any>
-
 export type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> }
 // type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> }
 
@@ -32,6 +33,10 @@ export type SubscriptionCallback<S> = (
   state: S
 ) => void
 
+/**
+ * Base store with state and functions
+ * @internal
+ */
 export interface StoreWithState<Id extends string, S extends StateTree> {
   /**
    * Unique identifier of the store
@@ -75,8 +80,9 @@ export interface StoreWithState<Id extends string, S extends StateTree> {
 
   /**
    * Setups a callback to be called whenever the state changes.
-   * @param callback - callback that is called whenever the state
-   * @returns function that removes callback from subscriptions
+   *
+   * @param callback - callback passed to the watcher
+   * @returns function that removes the watcher
    */
   $subscribe(callback: SubscriptionCallback<S>): () => void
 }
@@ -89,21 +95,24 @@ export type Method = (...args: any[]) => any
 // }
 
 // in this type we forget about this because otherwise the type is recursive
+/**
+ * Store augmented for actions
+ *
+ * @internal
+ */
 export type StoreWithActions<A> = {
   [k in keyof A]: A[k] extends (...args: infer P) => infer R
     ? (...args: P) => R
     : never
 }
 
-// export interface StoreGetter<S extends StateTree, T = any> {
-//   // TODO: would be nice to be able to define the getters here
-//   (state: S, getters: Record<string, Ref<any>>): T
-// }
-
+/**
+ * Store augmented with getters
+ *
+ * @internal
+ */
 export type StoreWithGetters<G> = {
-  [k in keyof G]: G[k] extends (this: infer This, store?: any) => infer R
-    ? R
-    : never
+  readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R ? R : never
 }
 
 // // in this type we forget about this because otherwise the type is recursive
@@ -114,10 +123,13 @@ export type StoreWithGetters<G> = {
 //     : never
 // }
 
+/**
+ * Store type to build a store
+ */
 export type Store<
   Id extends string,
   S extends StateTree,
-  G,
+  G extends GettersTree<S>,
   // has the actions without the context (this) for typings
   A
 > = StoreWithState<Id, S> &
@@ -126,13 +138,15 @@ export type Store<
   StoreWithActions<A> &
   PiniaCustomProperties<Id, S, G, A>
 
+// TODO: check if it's possible to add = to StoreDefinition and Store and cleanup GenericStore and the other one
+
 /**
  * Return type of `defineStore()`. Function that allows instantiating a store.
  */
 export interface StoreDefinition<
   Id extends string,
   S extends StateTree,
-  G /* extends Record<string, StoreGetterThis> */,
+  G extends GettersTree<S>,
   A /* extends Record<string, StoreAction> */
 > {
   (pinia?: Pinia | null | undefined): Store<Id, S, G, A>
@@ -145,7 +159,7 @@ export interface StoreDefinition<
 export type GenericStore = Store<
   string,
   StateTree,
-  Record<string, Method>,
+  GettersTree<StateTree>,
   Record<string, Method>
 >
 
@@ -155,38 +169,45 @@ export type GenericStore = Store<
 export type GenericStoreDefinition = StoreDefinition<
   string,
   StateTree,
-  Record<string, Method>,
+  GettersTree<StateTree>,
   Record<string, Method>
 >
 
-export interface DevtoolHook {
-  on(
-    event: string,
-    callback: (targetState: Record<string, StateTree>) => void
-  ): void
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  emit(event: string, ...payload: any[]): void
-}
-
-// add the __VUE_DEVTOOLS_GLOBAL_HOOK__ variable to the global namespace
-declare global {
-  interface Window {
-    __VUE_DEVTOOLS_GLOBAL_HOOK__?: DevtoolHook
-  }
-  namespace NodeJS {
-    interface Global {
-      __VUE_DEVTOOLS_GLOBAL_HOOK__?: DevtoolHook
-    }
-  }
-}
-
 /**
  * Properties that are added to every store by `pinia.use()`
  */
-// eslint-disable-next-line
 export interface PiniaCustomProperties<
   Id extends string = string,
   S extends StateTree = StateTree,
-  G = Record<string, Method>,
+  G extends GettersTree<S> = GettersTree<S>,
   A = Record<string, Method>
 > {}
+
+export type GettersTree<S extends StateTree> = Record<
+  string,
+  ((state: S) => any) | (() => any)
+>
+
+/**
+ * Options parameter of `defineStore()`. Can be extended to augment stores with
+ * the plugin API.
+ */
+export interface DefineStoreOptions<
+  Id extends string,
+  S extends StateTree,
+  G extends GettersTree<S>,
+  A /* extends Record<string, StoreAction> */
+> {
+  id: Id
+  state?: () => S
+  getters?: G & ThisType<S & StoreWithGetters<G> & PiniaCustomProperties>
+  // allow actions use other actions
+  actions?: A &
+    ThisType<
+      A &
+        S &
+        StoreWithState<Id, S> &
+        StoreWithGetters<G> &
+        PiniaCustomProperties
+    >
+}
index 66b8d2fa179cf96945e2bc0b688d1565050905fa..b9e69874bc861e15ebda75b58cd1da4e6582f72a 100644 (file)
@@ -1,21 +1,70 @@
-import { defineStore, expectType, mapStores } from '.'
+import { expectType, createPinia, defineStore, mapStores } from '.'
 
-declare module '../dist/src/index' {
+declare module '../dist/src' {
   export interface MapStoresCustomization {
-    // this is the only one that can be applied to work with other tests
     suffix: 'Store'
   }
+
+  export interface PiniaCustomProperties<Id, S, G, A> {
+    $actions: Array<keyof A>
+  }
+
+  export interface DefineStoreOptions<Id, S, G, A> {
+    debounce?: {
+      // Record<keyof A, number>
+      [k in keyof A]?: number
+    }
+  }
 }
 
-const useCounter = defineStore({
-  id: 'counter',
-  state: () => ({ n: 0 }),
+const pinia = createPinia()
+
+pinia.use((context) => {
+  expectType<string>(context.options.id)
+  expectType<string>(context.store.$id)
+
+  return {
+    $actions: Object.keys(context.options.actions || {}),
+  }
+})
+
+const useStore = defineStore({
+  id: 'main',
+  actions: {
+    one() {},
+    two() {
+      this.one()
+    },
+    three() {
+      this.two()
+    },
+  },
+
+  debounce: {
+    one: 200,
+    two: 300,
+    // three: 100
+  },
 })
 
-type CounterStore = ReturnType<typeof useCounter>
+type Procedure = (...args: any[]) => any
 
-const computedStores = mapStores(useCounter)
+function debounce<F extends Procedure>(fn: F, time = 200) {
+  return fn
+}
 
 expectType<{
-  counterStore: () => CounterStore
-}>(computedStores)
+  mainStore: () => ReturnType<typeof useStore>
+}>(mapStores(useStore))
+
+pinia.use(({ options, store }) => {
+  if (options.debounce) {
+    return Object.keys(options.debounce).reduce((debouncedActions, action) => {
+      debouncedActions[action] = debounce(
+        store[action],
+        options.debounce![action as keyof typeof options['actions']]
+      )
+      return debouncedActions
+    }, {} as Record<string, (...args: any[]) => any>)
+  }
+})
index 5acd82585111a707ffc44b1e8b939aa33b724fa0..43e40b7b975a342e15bd5377298044c68b21d8d3 100644 (file)
@@ -4,9 +4,7 @@ const useDeprecated = createStore({
   id: 'name',
   state: () => ({ a: 'on' as 'on' | 'off', nested: { counter: 0 } }),
   getters: {
-    upper() {
-      return this.a.toUpperCase()
-    },
+    upper: (state) => state.a.toUpperCase(),
   },
 })
 
index 8b77999268e4e68c2f3efba8ef8d14bd2297b3bf..72dbd8039798c4c5fd9661722668bd740a518743 100644 (file)
@@ -11,9 +11,7 @@ const useStore = defineStore({
   id: 'name',
   state: () => ({ a: 'on' as 'on' | 'off', nested: { counter: 0 } }),
   getters: {
-    upper() {
-      return this.a.toUpperCase()
-    },
+    upper: (state) => state.a.toUpperCase(),
   },
   actions: {
     toggleA() {
diff --git a/test-dts/plugins.test-d.ts b/test-dts/plugins.test-d.ts
new file mode 100644 (file)
index 0000000..ecd3ccc
--- /dev/null
@@ -0,0 +1,23 @@
+import {
+  expectType,
+  createPinia,
+  GenericStore,
+  Pinia,
+  StateTree,
+  DefineStoreOptions,
+} from '.'
+
+const pinia = createPinia()
+
+pinia.use(({ store, options, pinia }) => {
+  expectType<GenericStore>(store)
+  expectType<Pinia>(pinia)
+  expectType<
+    DefineStoreOptions<
+      string,
+      StateTree,
+      Record<string, any>,
+      Record<string, any>
+    >
+  >(options)
+})
index f068a4b6c3127b8aafd815099965a9fcee12759b..e31749846cefb6bcd011d509ce45fda634e6c4fd 100644 (file)
@@ -1,31 +1,51 @@
-import { GenericStore } from 'dist/src/types'
-import { defineStore, expectType, createPinia } from './'
-
-const pinia = createPinia()
-
-pinia.use(({ store }) => {
-  expectType<GenericStore>(store)
-})
+import { defineStore, expectType } from './'
 
 const useStore = defineStore({
   id: 'name',
   state: () => ({ a: 'on' as 'on' | 'off', nested: { counter: 0 } }),
   getters: {
-    upper() {
-      return this.a.toUpperCase()
+    upper: (state) => {
+      expectType<'on' | 'off'>(state.a)
+      return state.a.toUpperCase() as 'ON' | 'OFF'
+    },
+    upperThis(): 'ON' | 'OFF' {
+      expectType<'on' | 'off'>(this.a)
+      return this.a.toUpperCase() as 'ON' | 'OFF'
+    },
+    other(): false {
+      expectType<string>(this.upper)
+      return false
+    },
+
+    doubleCounter: (state) => {
+      expectType<number>(state.nested.counter)
+      return state.nested.counter * 2
+    },
+  },
+  actions: {
+    doStuff() {
+      expectType<string>(this.upper)
+      expectType<false>(this.other)
+    },
+    otherOne() {
+      expectType<() => void>(this.doStuff)
     },
   },
 })
 
-const store = useStore()
+let store = useStore()
 
 expectType<{ a: 'on' | 'off' }>(store.$state)
 expectType<number>(store.nested.counter)
 expectType<'on' | 'off'>(store.a)
+expectType<'ON' | 'OFF'>(store.upper)
 
 // @ts-expect-error
 store.nonExistant
 
+// @ts-expect-error
+store.upper = 'thing'
+
 // @ts-expect-error
 store.nonExistant.stuff