* @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>
] {
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
return removeSubscription
}
- function $onAction(callback: StoreOnActionListener) {
+ function $onAction(callback: StoreOnActionListener<Id, S, G, A>) {
actionSubscriptions.push(callback)
const removeSubscription = () => {
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
$subscribe,
$onAction,
$reset,
- } as StoreWithState<Id, S>
+ } as StoreWithState<Id, S, G, A>
const injectionSymbol = __DEV__
? Symbol(`PiniaStore(${$id})`)
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,
}
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]>
// apply all plugins
pinia._p.forEach((extender) => {
+ // @ts-expect-error: conflict between A and ActionsTree
assign(store, extender({ store, pinia, options }))
})
// 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>>
]
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
)
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>)
/**
* 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
*/
/**
* 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.
$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
*/
* @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
}
/**
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.
$id: Id
}
-/**
- * Generic version of Store.
- * @deprecated Use Store instead
- */
-export type GenericStore = Store
-
/**
* Properties that are added to every store by `pinia.use()`
*/
ThisType<
A &
UnwrapRef<S> &
- StoreWithState<Id, S> &
+ StoreWithState<Id, S, G, A> &
StoreWithGetters<G> &
PiniaCustomProperties &
PiniaCustomStateProperties
import {
expectType,
createPinia,
- GenericStore,
+ Store,
Pinia,
StateTree,
DefineStoreOptions,
const pinia = createPinia()
pinia.use(({ store, options, pinia }) => {
- expectType<GenericStore>(store)
+ expectType<Store>(store)
expectType<Pinia>(pinia)
expectType<
DefineStoreOptions<
-import { defineStore, expectType } from './'
+import { watch } from '@vue/composition-api'
+import { defineStore, expectType, Store, GenericStore } from './'
const useStore = defineStore({
id: 'name',
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')