Skip to content

Commit

Permalink
feat(types): infer args and returned value for onAction
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed May 12, 2021
1 parent 20e2a57 commit f3b3bcf
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 75 deletions.
10 changes: 5 additions & 5 deletions __tests__/onAction.spec.ts
Expand Up @@ -68,16 +68,16 @@ describe('Subscriptions', () => {

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()
Expand Down
7 changes: 4 additions & 3 deletions src/devtools/plugin.ts
Expand Up @@ -6,7 +6,7 @@ import {
GettersTree,
MutationType,
StateTree,
_Method,
ActionsTree,
} from '../types'
import {
formatEventData,
Expand Down Expand Up @@ -259,7 +259,7 @@ export function devtoolsPlugin<
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>

Expand Down Expand Up @@ -295,7 +295,8 @@ export function devtoolsPlugin<
}
}

addDevtools(app, store)
// FIXME: can this be fixed?
addDevtools(app, store as unknown as Store)

return { ...wrappedActions }
}
Expand Down
11 changes: 9 additions & 2 deletions src/mapHelpers.ts
Expand Up @@ -5,6 +5,7 @@ import {
StateTree,
Store,
StoreDefinition,
ActionsTree,
} from './types'

/**
Expand Down Expand Up @@ -55,14 +56,20 @@ function getCachedStore<
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'
Expand Down
3 changes: 2 additions & 1 deletion src/rootStore.ts
Expand Up @@ -8,6 +8,7 @@ import {
DefineStoreOptions,
Store,
GettersTree,
ActionsTree,
} from './types'

/**
Expand Down Expand Up @@ -119,7 +120,7 @@ export interface PiniaPluginContext<
Id extends string = string,
S extends StateTree = StateTree,
G extends GettersTree<S> = GettersTree<S>,
A = Record<string, _Method>
A /* extends ActionsTree */ = ActionsTree
> {
/**
* pinia instance.
Expand Down
66 changes: 45 additions & 21 deletions src/store.ts
Expand Up @@ -28,6 +28,8 @@ import {
GettersTree,
MutationType,
StoreOnActionListener,
UnwrapPromise,
ActionsTree,
} from './types'
import {
getActivePinia,
Expand Down Expand Up @@ -92,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>
] {
Expand All @@ -107,7 +114,7 @@ function initStore<Id extends string, S extends StateTree>(

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
Expand Down Expand Up @@ -199,7 +206,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 = () => {
Expand All @@ -220,7 +227,7 @@ 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: pinia,
_as: actionSubscriptions,
Expand All @@ -231,7 +238,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})`)
Expand Down Expand Up @@ -269,9 +276,9 @@ function buildStoreToUse<
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,
Expand All @@ -295,11 +302,11 @@ function buildStoreToUse<
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) {
Expand All @@ -310,13 +317,17 @@ function buildStoreToUse<
}

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
Expand Down Expand Up @@ -348,6 +359,7 @@ function buildStoreToUse<

// apply all plugins
pinia._p.forEach((extender) => {
// @ts-expect-error: conflict between A and ActionsTree
assign(store, extender({ store, app: pinia._a, pinia, options }))
})

Expand All @@ -362,7 +374,8 @@ export function defineStore<
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

Expand All @@ -380,23 +393,29 @@ export function defineStore<

let storeAndDescriptor = stores.get(id) as
| [
StoreWithState<Id, S>,
StoreWithState<Id, S, G, A>,
StateDescriptor<S>,
InjectionKey<Store<Id, S, G, A>>
]
| undefined
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
)

Expand All @@ -412,13 +431,18 @@ export function defineStore<
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
)
)
Expand Down

0 comments on commit f3b3bcf

Please sign in to comment.