From 65d4ab325ea20311bdcb41bd2e16ab873ae8a2dc Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 17 May 2021 18:37:49 +0200 Subject: [PATCH] fix(types): correct subtype Store Close #500 --- src/store.ts | 53 +++++++++++----- src/types.ts | 120 +++++++++++++++++++++++-------------- test-dts/plugins.test-d.ts | 4 +- test-dts/store.test-d.ts | 56 ++++++++++++++++- 4 files changed, 170 insertions(+), 63 deletions(-) diff --git a/src/store.ts b/src/store.ts index b6570283..2f54e29a 100644 --- a/src/store.ts +++ b/src/store.ts @@ -94,12 +94,17 @@ function computedFromState( * @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( +function initStore< + Id extends string, + S extends StateTree, + G extends GettersTree, + A /* extends ActionsTree */ +>( $id: Id, buildState: () => S = () => ({} as S), initialState?: S | undefined ): [ - StoreWithState, + StoreWithState, { get: () => S; set: (newValue: S) => void }, InjectionKey ] { @@ -109,7 +114,7 @@ function initStore( let isListening = true const subscriptions: SubscriptionCallback[] = [] - const actionSubscriptions: StoreOnActionListener[] = [] + const actionSubscriptions: StoreOnActionListener[] = [] function $patch(stateMutation: (state: S) => void): void function $patch(partialState: DeepPartial): void @@ -181,7 +186,7 @@ function initStore( return removeSubscription } - function $onAction(callback: StoreOnActionListener) { + function $onAction(callback: StoreOnActionListener) { actionSubscriptions.push(callback) const removeSubscription = () => { @@ -202,10 +207,10 @@ function initStore( pinia.state.value[$id] = buildState() } - const storeWithState: StoreWithState = { + const storeWithState: StoreWithState = { $id, _p: markRaw(pinia), - _as: actionSubscriptions, + _as: markRaw(actionSubscriptions as unknown as StoreOnActionListener[]), // $state is added underneath @@ -213,7 +218,7 @@ function initStore( $subscribe, $onAction, $reset, - } as StoreWithState + } as StoreWithState const injectionSymbol = __DEV__ ? Symbol(`PiniaStore(${$id})`) @@ -253,7 +258,7 @@ function buildStoreToUse< G extends GettersTree, A extends ActionsTree >( - partialStore: StoreWithState, + partialStore: StoreWithState, descriptor: StateDescriptor, $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 @@ -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 let storeAndDescriptor = stores.get(id) as | [ - StoreWithState, + StoreWithState, StateDescriptor, InjectionKey> ] @@ -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) diff --git a/src/types.ts b/src/types.ts index ee3a23c9..9ad83e3a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -149,47 +149,65 @@ export type SubscriptionCallback = ( /** * 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, + A /* extends ActionsTree */ +> = { + [Name in keyof A]: { + /** + * Name of the action + */ + name: Name + + /** + * Store that is invoking the action + */ + store: Store + + /** + * Parameters passed to the action + */ + args: A[Name] extends _Method ? Parameters : 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>) => 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 = GettersTree, + A /* extends ActionsTree */ = ActionsTree +> = (context: StoreOnActionListenerContext) => void /** * Base store with state and functions * @internal */ -export interface StoreWithState { +export interface StoreWithState< + Id extends string, + S extends StateTree, + G extends GettersTree = GettersTree, + A /* extends ActionsTree */ = ActionsTree +> { /** * Unique identifier of the store */ @@ -198,8 +216,7 @@ export interface StoreWithState { /** * State of the Store. Setting it will replace the whole state. */ - $state: (StateTree extends S ? {} : UnwrapRef) & - PiniaCustomStateProperties + $state: UnwrapRef & PiniaCustomStateProperties /** * Private property defining the pinia the store is attached to. @@ -243,7 +260,8 @@ export interface StoreWithState { $subscribe(callback: SubscriptionCallback): () => 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 { * @param callback - callback called before every action * @returns function that removes the watcher */ - $onAction(callback: StoreOnActionListener): () => void + $onAction(callback: StoreOnActionListener): () => void } /** @@ -335,15 +353,31 @@ export type Store< S extends StateTree = StateTree, G extends GettersTree = GettersTree, // has the actions without the context (this) for typings - A = ActionsTree -> = StoreWithState & + A /* extends ActionsTree */ = ActionsTree +> = StoreWithState & (StateTree extends S ? {} : UnwrapRef) & (GettersTree extends G ? {} : StoreWithGetters) & (ActionsTree extends A ? {} : StoreWithActions) & PiniaCustomProperties & PiniaCustomStateProperties -// 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 = GettersTree, + // has the actions without the context (this) for typings + A /* extends ActionsTree */ = ActionsTree +> = StoreWithState & + UnwrapRef & + StoreWithGetters & + StoreWithActions & + PiniaCustomProperties & + PiniaCustomStateProperties /** * 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 & - StoreWithState & + StoreWithState & StoreWithGetters & PiniaCustomProperties & PiniaCustomStateProperties diff --git a/test-dts/plugins.test-d.ts b/test-dts/plugins.test-d.ts index 3a2ce2fa..d1126d1a 100644 --- a/test-dts/plugins.test-d.ts +++ b/test-dts/plugins.test-d.ts @@ -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(store) + expectType(store) expectType(pinia) expectType< DefineStoreOptions< diff --git a/test-dts/store.test-d.ts b/test-dts/store.test-d.ts index 47bc00ee..70ab1938 100644 --- a/test-dts/store.test-d.ts +++ b/test-dts/store.test-d.ts @@ -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(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(genericStore.thing) +expectType(genericStore.$state.thing) +takeStore(genericStore) +useSyncValueToStore(() => 2, genericStore, 'myState') +useSyncValueToStore(() => 2, genericStore, 'random') +// @ts-expect-error +useSyncValueToStore(() => false, genericStore, 'myState') -- 2.47.3