it('calls after with the returned value', async () => {
const spy = jest.fn()
- store.$onAction(({ after, name, store }) => {
- name
- if (name === 'upperName') {
- after((ret) => {
+ // Cannot destructure because of https://github.com/microsoft/TypeScript/issues/38020
+ store.$onAction((context) => {
+ if (context.name === 'upperName') {
+ context.after((ret) => {
// @ts-expect-error
ret * 2
ret.toUpperCase()
})
}
- after(spy)
+ context.after(spy)
})
expect(store.upperName()).toBe('EDUARDO')
await nextTick()
GettersTree,
MutationType,
StateTree,
- _Method,
+ ActionsTree,
} from '../types'
import {
formatEventData,
Id extends string = string,
S extends StateTree = StateTree,
G extends GettersTree<S> = GettersTree<S>,
- A = Record<string, _Method>
+ A /* extends ActionsTree */ = ActionsTree
>({ app, store, options, pinia }: PiniaPluginContext<Id, S, G, A>) {
const wrappedActions = {} as Pick<typeof store, keyof A>
}
}
- addDevtools(app, store)
+ // FIXME: can this be fixed?
+ addDevtools(app, store as unknown as Store)
return { ...wrappedActions }
}
StateTree,
Store,
StoreDefinition,
+ ActionsTree,
} from './types'
/**
Id extends string = string,
S extends StateTree = StateTree,
G extends GettersTree<S> = GettersTree<S>,
- A = Record<string, _Method>
+ A /* extends ActionsTree */ = ActionsTree
>(
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>
+ return (cache[id] ||
+ (cache[id] = useStore(vm.$pinia) as unknown as Store)) as unknown as Store<
+ Id,
+ S,
+ G,
+ A
+ >
}
export let mapStoreSuffix = 'Store'
DefineStoreOptions,
Store,
GettersTree,
+ ActionsTree,
} from './types'
/**
Id extends string = string,
S extends StateTree = StateTree,
G extends GettersTree<S> = GettersTree<S>,
- A = Record<string, _Method>
+ A /* extends ActionsTree */ = ActionsTree
> {
/**
* pinia instance.
GettersTree,
MutationType,
StoreOnActionListener,
+ UnwrapPromise,
+ ActionsTree,
} from './types'
import {
getActivePinia,
* @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
let subscriptions: SubscriptionCallback<S>[] = []
- let actionSubscriptions: StoreOnActionListener[] = []
+ let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = []
let debuggerEvents: DebuggerEvent[] | DebuggerEvent
function $patch(stateMutation: (state: S) => void): 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: pinia,
_as: actionSubscriptions,
$subscribe,
$onAction,
$reset,
- } as StoreWithState<Id, S>
+ } as StoreWithState<Id, S, G, A>
const injectionSymbol = __DEV__
? Symbol(`PiniaStore(${$id})`)
Id extends string,
S extends StateTree,
G extends GettersTree<S>,
- A extends Record<string, _Method>
+ A extends ActionsTree
>(
- partialStore: StoreWithState<Id, S>,
+ partialStore: StoreWithState<Id, S, G, A>,
descriptor: StateDescriptor<S>,
$id: Id,
getters: G = {} as G,
for (const actionName in actions) {
wrappedActions[actionName] = function (this: Store<Id, S, G, A>) {
setActivePinia(pinia)
- const args = Array.from(arguments)
+ const args = Array.from(arguments) as Parameters<A[typeof actionName]>
const localStore = this || store
let afterCallback: (
- resolvedReturn: ReturnType<typeof actions[typeof actionName]>
+ resolvedReturn: UnwrapPromise<ReturnType<A[typeof actionName]>>
) => void = noop
let onErrorCallback: (error: unknown) => void = noop
function after(callback: typeof afterCallback) {
}
partialStore._as.forEach((callback) => {
+ // @ts-expect-error
callback({ args, name: actionName, store: localStore, after, onError })
})
- let ret: ReturnType<typeof actions[typeof actionName]>
+ let ret: ReturnType<A[typeof actionName]>
try {
ret = actions[actionName].apply(localStore, args as unknown as any[])
- Promise.resolve(ret).then(afterCallback).catch(onErrorCallback)
+ Promise.resolve(ret)
+ // @ts-expect-error: can't work this out
+ .then(afterCallback)
+ .catch(onErrorCallback)
} catch (error) {
onErrorCallback(error)
throw error
// apply all plugins
pinia._p.forEach((extender) => {
+ // @ts-expect-error: conflict between A and ActionsTree
assign(store, extender({ store, app: pinia._a, pinia, options }))
})
Id extends string,
S extends StateTree,
G extends GettersTree<S>,
- A /* extends Record<string, StoreAction> */
+ // cannot extends ActionsTree because we loose the typings
+ A /* extends ActionsTree */
>(options: DefineStoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A> {
const { id, state, getters, actions } = options
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])
- stores.set(id, storeAndDescriptor)
+ // annoying to type
+ stores.set(id, storeAndDescriptor as any)
- const store = buildStoreToUse(
+ const store = buildStoreToUse<
+ Id,
+ S,
+ G,
+ // @ts-expect-error: A without extends
+ A
+ >(
storeAndDescriptor[0],
storeAndDescriptor[1],
id,
getters as GettersTree<S> | undefined,
- actions as Record<string, _Method> | undefined,
- // @ts-expect-error: because of the extend on Actions
+ actions as A | undefined,
options
)
return (
// null avoids the warning for not found injection key
(hasInstance && inject(storeAndDescriptor[2], null)) ||
- buildStoreToUse(
+ buildStoreToUse<
+ Id,
+ S,
+ G,
+ // @ts-expect-error: A without extends
+ A
+ >(
storeAndDescriptor[0],
storeAndDescriptor[1],
id,
getters as GettersTree<S> | undefined,
- actions as Record<string, _Method> | undefined,
- // @ts-expect-error: because of the extend on Actions
+ actions as A | undefined,
options
)
)
// maybe reset? for $state = {} and $reset
}
+export type UnwrapPromise<T> = T extends Promise<infer V> ? V : T
+
/**
* 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
+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
- /**
- * Sets up a hook if the action fails.
- */
- onError: (callback: (error: unknown) => void) => void
+ /**
+ * Store that is invoking the action
+ */
+ store: Store<Id, S, G, A>
- // TODO: pass generics
- /**
- * Store that is invoking the action
- */
- store: GenericStore
+ /**
+ * Parameters passed to the action
+ */
+ args: A[Name] extends _Method ? Parameters<A[Name]> : unknown[]
- /**
- * Name of the action
- */
- name: string
+ /**
+ * 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
- /**
- * Parameters passed to the action
- */
- args: any[]
-}
+ /**
+ * 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,
+ S extends StateTree,
+ G extends GettersTree<S>,
+ A /* extends ActionsTree */
+> = (context: StoreOnActionListenerContext<Id, S, G, A>) => void
/**
* Callback of a subscription
* 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<S> = GettersTree<S>,
+ A /* extends ActionsTree */ = ActionsTree
+> {
/**
* Unique identifier of the store
*/
*
* @internal
*/
- _as: StoreOnActionListener[]
+ _as: StoreOnActionListener<Id, S, G, A>[]
/**
* @alpha Please send feedback at https://github.com/posva/pinia/issues/240
* @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
}
/**
* Store type to build a store
*/
export type Store<
- Id extends string,
- S extends StateTree,
- G extends GettersTree<S>,
+ Id extends string = string,
+ S extends StateTree = StateTree,
+ G extends GettersTree<S> = GettersTree<S>,
// has the actions without the context (this) for typings
- A
-> = StoreWithState<Id, S> &
+ A /* extends ActionsTree */ = ActionsTree
+> = StoreWithState<Id, S, G, A> &
UnwrapRef<S> &
StoreWithGetters<G> &
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 GettersTree<S>,
- A /* extends Record<string, StoreAction> */
+ Id extends string = string,
+ S extends StateTree = StateTree,
+ G extends GettersTree<S> = GettersTree<S>,
+ A /* extends ActionsTree */ = ActionsTree
> {
/**
* Returns a store, creates it if necessary.
Id extends string = string,
S extends StateTree = StateTree,
G extends GettersTree<S> = GettersTree<S>,
- A = Record<string, _Method>
+ A /* extends ActionsTree */ = ActionsTree
> {}
/**
((state: UnwrapRef<S>) => any) | (() => any)
>
+/**
+ * Type of an object of Actions
+ *
+ * @internal
+ */
+export type ActionsTree = Record<string, _Method>
+
/**
* Options parameter of `defineStore()`. Can be extended to augment stores with
* the plugin API.
ThisType<
A &
UnwrapRef<S> &
- StoreWithState<Id, S> &
+ StoreWithState<Id, S, G, A> &
StoreWithGetters<G> &
PiniaCustomProperties
>
--- /dev/null
+import { defineStore, expectType } from '.'
+
+const useStore = defineStore({
+ id: 'main',
+ state: () => ({
+ user: 'Eduardo',
+ }),
+ actions: {
+ direct(name: string) {
+ this.user = name
+ },
+ patchObject(user: string) {
+ this.$patch({ user })
+ },
+ patchFn(name: string) {
+ this.$patch((state) => {
+ state.user = name
+ })
+ },
+ async asyncUpperName() {
+ return this.user.toUpperCase()
+ },
+ upperName() {
+ return this.user.toUpperCase()
+ },
+ throws(e: any) {
+ throw e
+ },
+ async rejects(e: any) {
+ throw e
+ },
+ },
+})
+
+let store = useStore()
+
+store.$onAction((context) => {
+ expectType<{ user: string }>(context.store.$state)
+ expectType<(name: string) => void>(context.store.direct)
+
+ if (context.name === 'upperName') {
+ expectType<[]>(context.args)
+ context.after((ret) => {
+ expectType<string>(ret)
+ })
+ } else if (context.name === 'asyncUpperName') {
+ context.after((ret) => {
+ expectType<string>(ret)
+ })
+ } else if (context.name === 'throws') {
+ context.after((ret) => {
+ expectType<never>(ret)
+ })
+ expectType<[any]>(context.args)
+ } else if (context.name === 'direct') {
+ expectType<[string]>(context.args)
+ }
+})