]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
fix(types): correct subtype Store
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 17 May 2021 16:37:49 +0000 (18:37 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 17 May 2021 16:54:53 +0000 (18:54 +0200)
Close #500

src/store.ts
src/types.ts
test-dts/plugins.test-d.ts
test-dts/store.test-d.ts

index b6570283fd0ba3d2795e2f64616a4b9f3dd1a506..2f54e29a1e0bf3f36fa64fdaeffd4f2e1356b467 100644 (file)
@@ -94,12 +94,17 @@ function computedFromState<T, Id extends string>(
  * @param buildState - function to build the initial state
  * @param initialState - initial state applied to the store, Must be correctly typed to infer typings
  */
-function initStore<Id extends string, S extends StateTree>(
+function initStore<
+  Id extends string,
+  S extends StateTree,
+  G extends GettersTree<S>,
+  A /* extends ActionsTree */
+>(
   $id: Id,
   buildState: () => S = () => ({} as S),
   initialState?: S | undefined
 ): [
-  StoreWithState<Id, S>,
+  StoreWithState<Id, S, G, A>,
   { get: () => S; set: (newValue: S) => void },
   InjectionKey<Store>
 ] {
@@ -109,7 +114,7 @@ function initStore<Id extends string, S extends StateTree>(
 
   let isListening = true
   const subscriptions: SubscriptionCallback<S>[] = []
-  const actionSubscriptions: StoreOnActionListener[] = []
+  const actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = []
 
   function $patch(stateMutation: (state: S) => void): void
   function $patch(partialState: DeepPartial<S>): void
@@ -181,7 +186,7 @@ function initStore<Id extends string, S extends StateTree>(
     return removeSubscription
   }
 
-  function $onAction(callback: StoreOnActionListener) {
+  function $onAction(callback: StoreOnActionListener<Id, S, G, A>) {
     actionSubscriptions.push(callback)
 
     const removeSubscription = () => {
@@ -202,10 +207,10 @@ function initStore<Id extends string, S extends StateTree>(
     pinia.state.value[$id] = buildState()
   }
 
-  const storeWithState: StoreWithState<Id, S> = {
+  const storeWithState: StoreWithState<Id, S, G, A> = {
     $id,
     _p: markRaw(pinia),
-    _as: actionSubscriptions,
+    _as: markRaw(actionSubscriptions as unknown as StoreOnActionListener[]),
 
     // $state is added underneath
 
@@ -213,7 +218,7 @@ function initStore<Id extends string, S extends StateTree>(
     $subscribe,
     $onAction,
     $reset,
-  } as StoreWithState<Id, S>
+  } as StoreWithState<Id, S, G, A>
 
   const injectionSymbol = __DEV__
     ? Symbol(`PiniaStore(${$id})`)
@@ -253,7 +258,7 @@ function buildStoreToUse<
   G extends GettersTree<S>,
   A extends ActionsTree
 >(
-  partialStore: StoreWithState<Id, S>,
+  partialStore: StoreWithState<Id, S, G, A>,
   descriptor: StateDescriptor<S>,
   $id: Id,
   getters: G = {} as G,
@@ -293,7 +298,14 @@ function buildStoreToUse<
       }
 
       partialStore._as.forEach((callback) => {
-        callback({ args, name: actionName, store: localStore, after, onError })
+        callback({
+          args,
+          name: actionName,
+          // @ts-expect-error
+          store: localStore,
+          after,
+          onError,
+        })
       })
 
       let ret: ReturnType<typeof actions[typeof actionName]>
@@ -327,6 +339,7 @@ function buildStoreToUse<
 
   // apply all plugins
   pinia._p.forEach((extender) => {
+    // @ts-expect-error: conflict between A and ActionsTree
     assign(store, extender({ store, pinia, options }))
   })
 
@@ -364,7 +377,7 @@ export function defineStore<
     // let store = stores.get(id) as Store<Id, S, G, A>
     let storeAndDescriptor = stores.get(id) as
       | [
-          StoreWithState<Id, S>,
+          StoreWithState<Id, S, G, A>,
           StateDescriptor<S>,
           InjectionKey<Store<Id, S, G, A>>
         ]
@@ -373,18 +386,25 @@ export function defineStore<
     if (!storeAndDescriptor) {
       storeAndDescriptor = initStore(id, state, pinia.state.value[id])
 
+      // @ts-expect-error: annoying to type
       stores.set(id, storeAndDescriptor)
 
       if (__DEV__ && isClient) {
+        // @ts-expect-error: annoying to type
         useStoreDevtools(storeAndDescriptor[0], storeAndDescriptor[1])
       }
 
-      const store = buildStoreToUse(
+      const store = buildStoreToUse<
+        Id,
+        S,
+        G,
+        // @ts-expect-error: cannot extends ActionsTree
+        A
+      >(
         storeAndDescriptor[0],
         storeAndDescriptor[1],
         id,
         getters,
-        // @ts-expect-error: all good
         actions,
         options
       )
@@ -400,12 +420,17 @@ export function defineStore<
 
     return (
       (hasInstance && inject(storeAndDescriptor[2], null)) ||
-      (buildStoreToUse(
+      (buildStoreToUse<
+        Id,
+        S,
+        G,
+        // @ts-expect-error: cannot extends ActionsTree
+        A
+      >(
         storeAndDescriptor[0],
         storeAndDescriptor[1],
         id,
         getters,
-        // @ts-expect-error: all good
         actions,
         options
       ) as Store<Id, S, G, A>)
index ee3a23c9744e99b38a56803985c5026c0b729e2c..9ad83e3a28eb458cc8a9024b960e26510017d966 100644 (file)
@@ -149,47 +149,65 @@ export type SubscriptionCallback<S> = (
 /**
  * Context object passed to callbacks of `store.$onAction(context => {})`
  */
-export interface StoreOnActionListenerContext {
-  /**
-   * Sets up a hook once the action is finished. It receives the return value of
-   * the action, if it's a Promise, it will be unwrapped.
-   */
-  after: (callback: (resolvedReturn: unknown) => void) => void
-
-  /**
-   * Sets up a hook if the action fails.
-   */
-  onError: (callback: (error: unknown) => void) => void
-
-  // TODO: pass generics
-  /**
-   * Store that is invoking the action
-   */
-  store: GenericStore
-
-  /**
-   * Name of the action
-   */
-  name: string
-
-  /**
-   * Parameters passed to the action
-   */
-  args: any[]
-}
+export type StoreOnActionListenerContext<
+  Id extends string,
+  S extends StateTree,
+  G extends GettersTree<S>,
+  A /* extends ActionsTree */
+> = {
+  [Name in keyof A]: {
+    /**
+     * Name of the action
+     */
+    name: Name
+
+    /**
+     * Store that is invoking the action
+     */
+    store: Store<Id, S, G, A>
+
+    /**
+     * Parameters passed to the action
+     */
+    args: A[Name] extends _Method ? Parameters<A[Name]> : unknown[]
+
+    /**
+     * Sets up a hook once the action is finished. It receives the return value of
+     * the action, if it's a Promise, it will be unwrapped.
+     */
+    after: (
+      callback: A[Name] extends _Method
+        ? (resolvedReturn: UnwrapPromise<ReturnType<A[Name]>>) => void
+        : () => void
+    ) => void
+
+    /**
+     * Sets up a hook if the action fails.
+     */
+    onError: (callback: (error: unknown) => void) => void
+  }
+}[keyof A]
 
 /**
  * Argument of `store.$onAction()`
  */
-export type StoreOnActionListener = (
-  context: StoreOnActionListenerContext
-) => void
+export type StoreOnActionListener<
+  Id extends string = string,
+  S extends StateTree = StateTree,
+  G extends GettersTree<S> = GettersTree<S>,
+  A /* extends ActionsTree */ = ActionsTree
+> = (context: StoreOnActionListenerContext<Id, S, G, A>) => void
 
 /**
  * Base store with state and functions
  * @internal
  */
-export interface StoreWithState<Id extends string, S extends StateTree> {
+export interface StoreWithState<
+  Id extends string,
+  S extends StateTree,
+  G extends GettersTree<StateTree> = GettersTree<S>,
+  A /* extends ActionsTree */ = ActionsTree
+> {
   /**
    * Unique identifier of the store
    */
@@ -198,8 +216,7 @@ export interface StoreWithState<Id extends string, S extends StateTree> {
   /**
    * State of the Store. Setting it will replace the whole state.
    */
-  $state: (StateTree extends S ? {} : UnwrapRef<S>) &
-    PiniaCustomStateProperties<S>
+  $state: UnwrapRef<S> & PiniaCustomStateProperties<S>
 
   /**
    * Private property defining the pinia the store is attached to.
@@ -243,7 +260,8 @@ export interface StoreWithState<Id extends string, S extends StateTree> {
   $subscribe(callback: SubscriptionCallback<S>): () => void
 
   /**
-   * Array of registered action subscriptions.
+   * Array of registered action subscriptions.Set without the generics to avoid
+   * errors between the generic version of Store and specific stores.
    *
    * @internal
    */
@@ -283,7 +301,7 @@ export interface StoreWithState<Id extends string, S extends StateTree> {
    * @param callback - callback called before every action
    * @returns function that removes the watcher
    */
-  $onAction(callback: StoreOnActionListener): () => void
+  $onAction(callback: StoreOnActionListener<Id, S, G, A>): () => void
 }
 
 /**
@@ -335,15 +353,31 @@ export type Store<
   S extends StateTree = StateTree,
   G extends GettersTree<S> = GettersTree<S>,
   // has the actions without the context (this) for typings
-  A = ActionsTree
-> = StoreWithState<Id, S> &
+  A /* extends ActionsTree */ = ActionsTree
+> = StoreWithState<Id, StateTree extends S ? {} : S, G, A> &
   (StateTree extends S ? {} : UnwrapRef<S>) &
   (GettersTree<S> extends G ? {} : StoreWithGetters<G>) &
   (ActionsTree extends A ? {} : StoreWithActions<A>) &
   PiniaCustomProperties<Id, S, G, A> &
   PiniaCustomStateProperties<S>
 
-// TODO: check if it's possible to add = to StoreDefinition and Store and cleanup GenericStore and the other one
+/**
+ * Generic and type-unsafe version of Store. Doesn't fail on access with
+ * strings, making it much easier to write generic functions that do not care
+ * about the kind of store that is passed.
+ */
+export type GenericStore<
+  Id extends string = string,
+  S extends StateTree = StateTree,
+  G extends GettersTree<S> = GettersTree<S>,
+  // has the actions without the context (this) for typings
+  A /* extends ActionsTree */ = ActionsTree
+> = StoreWithState<Id, S, G, A> &
+  UnwrapRef<S> &
+  StoreWithGetters<G> &
+  StoreWithActions<A> &
+  PiniaCustomProperties<Id, S, G, A> &
+  PiniaCustomStateProperties<S>
 
 /**
  * Return type of `defineStore()`. Function that allows instantiating a store.
@@ -367,12 +401,6 @@ export interface StoreDefinition<
   $id: Id
 }
 
-/**
- * Generic version of Store.
- * @deprecated Use Store instead
- */
-export type GenericStore = Store
-
 /**
  * Properties that are added to every store by `pinia.use()`
  */
@@ -438,7 +466,7 @@ export interface DefineStoreOptions<
     ThisType<
       A &
         UnwrapRef<S> &
-        StoreWithState<Id, S> &
+        StoreWithState<Id, S, G, A> &
         StoreWithGetters<G> &
         PiniaCustomProperties &
         PiniaCustomStateProperties
index 3a2ce2fad30eb073e53fcdbf6747bf5fb1248603..d1126d1a528a6e5f5854c84e709c671a17baf23b 100644 (file)
@@ -1,7 +1,7 @@
 import {
   expectType,
   createPinia,
-  GenericStore,
+  Store,
   Pinia,
   StateTree,
   DefineStoreOptions,
@@ -10,7 +10,7 @@ import {
 const pinia = createPinia()
 
 pinia.use(({ store, options, pinia }) => {
-  expectType<GenericStore>(store)
+  expectType<Store>(store)
   expectType<Pinia>(pinia)
   expectType<
     DefineStoreOptions<
index 47bc00ee8284166846c30f9894f7c94b9a39787a..70ab1938d1b9382f9fc832c94b0af9733f439e2f 100644 (file)
@@ -1,4 +1,5 @@
-import { defineStore, expectType } from './'
+import { watch } from '@vue/composition-api'
+import { defineStore, expectType, Store, GenericStore } from './'
 
 const useStore = defineStore({
   id: 'name',
@@ -118,3 +119,56 @@ noS.notExisting
 noA.notExisting
 // @ts-expect-error
 noG.notExisting
+
+function takeStore<TStore extends Store>(store: TStore): TStore['$id'] {
+  return store.$id
+}
+
+export const useSyncValueToStore = <
+  TStore extends Store,
+  TKey extends keyof TStore['$state']
+>(
+  propGetter: () => TStore[TKey],
+  store: TStore,
+  key: TKey
+): void => {
+  watch(
+    propGetter,
+    (propValue) => {
+      store[key] = propValue
+    },
+    {
+      immediate: true,
+    }
+  )
+}
+
+useSyncValueToStore(() => 'on' as const, store, 'a')
+// @ts-expect-error
+useSyncValueToStore(() => true, store, 'a')
+takeStore(store)
+takeStore(noSAG)
+// @ts-expect-error
+useSyncValueToStore(() => 2, noSAG, 'nope')
+// @ts-expect-error
+useSyncValueToStore(() => null, noSAG, 'myState')
+takeStore(noSA)
+takeStore(noAG)
+useSyncValueToStore(() => 2, noAG, 'myState')
+takeStore(noSG)
+takeStore(noS)
+takeStore(noA)
+useSyncValueToStore(() => 2, noA, 'myState')
+takeStore(noG)
+useSyncValueToStore(() => 2, noG, 'myState')
+
+declare let genericStore: GenericStore
+
+// should not fail like it does with Store
+expectType<any>(genericStore.thing)
+expectType<any>(genericStore.$state.thing)
+takeStore(genericStore)
+useSyncValueToStore(() => 2, genericStore, 'myState')
+useSyncValueToStore(() => 2, genericStore, 'random')
+// @ts-expect-error
+useSyncValueToStore(() => false, genericStore, 'myState')