From: Eduardo San Martin Morote Date: Mon, 3 May 2021 08:28:42 +0000 (+0200) Subject: feat(store): pass state to getters as first argument X-Git-Tag: v0.4.0~13 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=9f3132bd7a59c47f3bd9005695c4f1224dbaea16;p=thirdparty%2Fvuejs%2Fpinia.git feat(store): pass state to getters as first argument BREAKING CHANGE: getters now receive the state as their first argument and it's properly typed so you can write getters with arrow functions: ```js defineStore({ state: () => ({ n: 0 }), getters: { double: state => state.n * 2 } }) ``` To access other getters, you must still use the syntax that uses `this` **but it is now necessary to explicitely type the getter return type**. The same limitation exists in Vue for computed properties and it's a known limitation in TypeScript: ```ts defineStore({ state: () => ({ n: 0 }), getters: { double: state => state.n * 2, // the `: number` is necessary when accessing `this` inside of // a getter doublePlusOne(state): number { return this.double + 1 }, } }) ``` For more information, refer to [the updated documentation for getters](https://pinia.esm.dev/core-concepts/getters.html). --- diff --git a/.eslintrc.js b/.eslintrc.js index a1de9499..e2eefaff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,5 +17,6 @@ module.exports = { '@typescript-eslint/no-namespace': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-empty-interface': 'off', }, } diff --git a/src/mapHelpers.ts b/src/mapHelpers.ts index b76d72ea..72b8f27e 100644 --- a/src/mapHelpers.ts +++ b/src/mapHelpers.ts @@ -1,13 +1,15 @@ import type Vue from 'vue' import { GenericStore, - GenericStoreDefinition, + GettersTree, Method, StateTree, Store, StoreDefinition, } from './types' +type ComponentPublicInstance = Vue + /** * Interface to allow customizing map helpers. Extend this interface with the * following properties: @@ -49,10 +51,13 @@ type Spread = A extends [infer L, ...infer R] function getCachedStore< Id extends string = string, S extends StateTree = StateTree, - G = Record, + G extends GettersTree = GettersTree, A = Record ->(vm: Vue, useStore: StoreDefinition): Store { - const cache = vm._pStores || (vm._pStores = {}) +>( + vm: ComponentPublicInstance, + useStore: StoreDefinition +): Store { + const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {}) const id = useStore.$id return (cache[id] || (cache[id] = useStore(vm.$pinia))) as Store } @@ -96,9 +101,21 @@ export function setMapStoreSuffix( * * @param stores - list of stores to map to an object */ -export function mapStores( +export function mapStores( ...stores: [...Stores] ): Spread { + if (__DEV__ && Array.isArray(stores[0])) { + console.warn( + `[🍍]: Directly pass all stores to "mapStores()" without putting them in an array:\n` + + `Replace\n` + + `\tmapStores([useAuthStore, useCartStore])\n` + + `with\n` + + `\tmapStores(useAuthStore, useCartStore)\n` + + `This will fail in production if not fixed.` + ) + stores = stores[0] + } + return stores.reduce((reduced, useStore) => { // @ts-ignore: $id is added by defineStore reduced[useStore.$id + mapStoreSuffix] = function (this: Vue) { @@ -108,14 +125,14 @@ export function mapStores( }, {} as Spread) } -type MapStateReturn = { +type MapStateReturn> = { [key in keyof S | keyof G]: () => Store[key] } type MapStateObjectReturn< Id extends string, S extends StateTree, - G, + G extends GettersTree, A, T extends Record< string, @@ -168,7 +185,7 @@ type MapStateObjectReturn< export function mapState< Id extends string, S extends StateTree, - G, + G extends GettersTree, A, KeyMapper extends Record< string, @@ -201,7 +218,12 @@ export function mapState< * @param useStore - store to map from * @param keys - array of state properties or getters */ -export function mapState( +export function mapState< + Id extends string, + S extends StateTree, + G extends GettersTree, + A +>( useStore: StoreDefinition, keys: Array ): MapStateReturn @@ -216,7 +238,7 @@ export function mapState( export function mapState< Id extends string, S extends StateTree, - G, + G extends GettersTree, A, KeyMapper extends Record< string, @@ -228,13 +250,13 @@ export function mapState< ): MapStateReturn | MapStateObjectReturn { return Array.isArray(keysOrMapper) ? keysOrMapper.reduce((reduced, key) => { - reduced[key] = function (this: Vue) { + reduced[key] = function (this: ComponentPublicInstance) { return getCachedStore(this, useStore)[key] } as () => any return reduced }, {} as MapStateReturn) : Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => { - reduced[key] = function (this: Vue) { + reduced[key] = function (this: ComponentPublicInstance) { const store = getCachedStore(this, useStore) const storeKey = keysOrMapper[key] // for some reason TS is unable to infer the type of storeKey to be a @@ -249,7 +271,7 @@ export function mapState< /** * Alias for `mapState()`. You should use `mapState()` instead. - * @deprecated + * @deprecated use `mapState()` instead. */ export const mapGetters = mapState @@ -289,7 +311,7 @@ type MapActionsObjectReturn> = { export function mapActions< Id extends string, S extends StateTree, - G, + G extends GettersTree, A, KeyMapper extends Record >( @@ -319,7 +341,12 @@ export function mapActions< * @param useStore - store to map from * @param keys - array of action names to map */ -export function mapActions( +export function mapActions< + Id extends string, + S extends StateTree, + G extends GettersTree, + A +>( useStore: StoreDefinition, keys: Array ): MapActionsReturn @@ -334,7 +361,7 @@ export function mapActions( export function mapActions< Id extends string, S extends StateTree, - G, + G extends GettersTree, A, KeyMapper extends Record >( @@ -343,13 +370,19 @@ export function mapActions< ): MapActionsReturn | MapActionsObjectReturn { return Array.isArray(keysOrMapper) ? keysOrMapper.reduce((reduced, key) => { - reduced[key] = function (this: Vue, ...args: any[]) { + reduced[key] = function ( + this: ComponentPublicInstance, + ...args: any[] + ) { return (getCachedStore(this, useStore)[key] as Method)(...args) } as Store[keyof A] return reduced }, {} as MapActionsReturn) : Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => { - reduced[key] = function (this: Vue, ...args: any[]) { + reduced[key] = function ( + this: ComponentPublicInstance, + ...args: any[] + ) { return getCachedStore(this, useStore)[keysOrMapper[key]](...args) } as Store[keyof KeyMapper[]] return reduced @@ -384,7 +417,7 @@ type MapWritableStateObjectReturn< export function mapWritableState< Id extends string, S extends StateTree, - G, + G extends GettersTree, A, KeyMapper extends Record >( @@ -399,7 +432,12 @@ export function mapWritableState< * @param useStore - store to map from * @param keys - array of state properties */ -export function mapWritableState( +export function mapWritableState< + Id extends string, + S extends StateTree, + G extends GettersTree, + A +>( useStore: StoreDefinition, keys: Array ): MapWritableStateReturn @@ -414,7 +452,7 @@ export function mapWritableState( export function mapWritableState< Id extends string, S extends StateTree, - G, + G extends GettersTree, A, KeyMapper extends Record >( @@ -423,11 +461,12 @@ export function mapWritableState< ): MapWritableStateReturn | MapWritableStateObjectReturn { return Array.isArray(keysOrMapper) ? keysOrMapper.reduce((reduced, key) => { + // @ts-ignore reduced[key] = { - get(this: Vue) { + get(this: ComponentPublicInstance) { return getCachedStore(this, useStore)[key] }, - set(this: Vue, value) { + set(this: ComponentPublicInstance, value) { // it's easier to type it here as any return (getCachedStore(this, useStore)[key] = value as any) }, @@ -437,10 +476,10 @@ export function mapWritableState< : Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => { // @ts-ignore reduced[key] = { - get(this: Vue) { + get(this: ComponentPublicInstance) { return getCachedStore(this, useStore)[keysOrMapper[key]] }, - set(this: Vue, value) { + set(this: ComponentPublicInstance, value) { // it's easier to type it here as any return (getCachedStore(this, useStore)[ keysOrMapper[key] diff --git a/src/store.ts b/src/store.ts index 388c3a65..5fc6b932 100644 --- a/src/store.ts +++ b/src/store.ts @@ -21,6 +21,7 @@ import { StateDescriptor, PiniaCustomProperties, StoreDefinition, + GettersTree, } from './types' import { useStoreDevtools } from './devtools' import { @@ -200,7 +201,7 @@ function initStore( function buildStoreToUse< Id extends string, S extends StateTree, - G extends Record, + G extends GettersTree, A extends Record >( partialStore: StoreWithState, @@ -213,9 +214,11 @@ function buildStoreToUse< const computedGetters: StoreWithGetters = {} as StoreWithGetters for (const getterName in getters) { + // @ts-expect-error: it's only readonly for the users computedGetters[getterName] = computed(() => { setActivePinia(pinia) // eslint-disable-next-line @typescript-eslint/no-use-before-define + // @ts-expect-error: the argument count is correct return getters[getterName].call(store, store) }) as StoreWithGetters[typeof getterName] } @@ -262,7 +265,7 @@ function buildStoreToUse< export function defineStore< Id extends string, S extends StateTree, - G /* extends Record */, + G extends GettersTree, A /* extends Record */ >(options: { id: Id @@ -310,7 +313,7 @@ export function defineStore< storeAndDescriptor[0], storeAndDescriptor[1], id, - getters as Record | undefined, + getters as GettersTree | undefined, actions as Record | undefined ) @@ -321,7 +324,7 @@ export function defineStore< storeAndDescriptor[0], storeAndDescriptor[1], id, - getters as Record | undefined, + getters as GettersTree | undefined, actions as Record | undefined ) } diff --git a/src/types.ts b/src/types.ts index 8ed83209..d0d9fa39 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,8 @@ import { Pinia } from './rootStore' +/** + * Generic state of a Store + */ export type StateTree = Record /** @@ -22,8 +25,6 @@ export function isPlainObject( ) } -export type NonNullObject = Record - export type DeepPartial = { [K in keyof T]?: DeepPartial } // type DeepReadonly = { readonly [P in keyof T]: DeepReadonly } @@ -32,6 +33,10 @@ export type SubscriptionCallback = ( state: S ) => void +/** + * Base store with state and functions + * @internal + */ export interface StoreWithState { /** * Unique identifier of the store @@ -75,8 +80,9 @@ export interface StoreWithState { /** * Setups a callback to be called whenever the state changes. - * @param callback - callback that is called whenever the state - * @returns function that removes callback from subscriptions + * + * @param callback - callback passed to the watcher + * @returns function that removes the watcher */ $subscribe(callback: SubscriptionCallback): () => void } @@ -89,21 +95,24 @@ export type Method = (...args: any[]) => any // } // in this type we forget about this because otherwise the type is recursive +/** + * Store augmented for actions + * + * @internal + */ export type StoreWithActions = { [k in keyof A]: A[k] extends (...args: infer P) => infer R ? (...args: P) => R : never } -// export interface StoreGetter { -// // TODO: would be nice to be able to define the getters here -// (state: S, getters: Record>): T -// } - +/** + * Store augmented with getters + * + * @internal + */ export type StoreWithGetters = { - [k in keyof G]: G[k] extends (this: infer This, store?: any) => infer R - ? R - : never + readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R ? R : never } // // in this type we forget about this because otherwise the type is recursive @@ -114,10 +123,13 @@ export type StoreWithGetters = { // : never // } +/** + * Store type to build a store + */ export type Store< Id extends string, S extends StateTree, - G, + G extends GettersTree, // has the actions without the context (this) for typings A > = StoreWithState & @@ -126,13 +138,15 @@ export type Store< StoreWithActions & PiniaCustomProperties +// 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 Record */, + G extends GettersTree, A /* extends Record */ > { (pinia?: Pinia | null | undefined): Store @@ -145,7 +159,7 @@ export interface StoreDefinition< export type GenericStore = Store< string, StateTree, - Record, + GettersTree, Record > @@ -155,38 +169,45 @@ export type GenericStore = Store< export type GenericStoreDefinition = StoreDefinition< string, StateTree, - Record, + GettersTree, Record > -export interface DevtoolHook { - on( - event: string, - callback: (targetState: Record) => void - ): void - // eslint-disable-next-line @typescript-eslint/no-explicit-any - emit(event: string, ...payload: any[]): void -} - -// add the __VUE_DEVTOOLS_GLOBAL_HOOK__ variable to the global namespace -declare global { - interface Window { - __VUE_DEVTOOLS_GLOBAL_HOOK__?: DevtoolHook - } - namespace NodeJS { - interface Global { - __VUE_DEVTOOLS_GLOBAL_HOOK__?: DevtoolHook - } - } -} - /** * Properties that are added to every store by `pinia.use()` */ -// eslint-disable-next-line export interface PiniaCustomProperties< Id extends string = string, S extends StateTree = StateTree, - G = Record, + G extends GettersTree = GettersTree, A = Record > {} + +export type GettersTree = Record< + string, + ((state: S) => any) | (() => any) +> + +/** + * Options parameter of `defineStore()`. Can be extended to augment stores with + * the plugin API. + */ +export interface DefineStoreOptions< + Id extends string, + S extends StateTree, + G extends GettersTree, + A /* extends Record */ +> { + id: Id + state?: () => S + getters?: G & ThisType & PiniaCustomProperties> + // allow actions use other actions + actions?: A & + ThisType< + A & + S & + StoreWithState & + StoreWithGetters & + PiniaCustomProperties + > +} diff --git a/test-dts/customizations.test-d.ts b/test-dts/customizations.test-d.ts index 66b8d2fa..b9e69874 100644 --- a/test-dts/customizations.test-d.ts +++ b/test-dts/customizations.test-d.ts @@ -1,21 +1,70 @@ -import { defineStore, expectType, mapStores } from '.' +import { expectType, createPinia, defineStore, mapStores } from '.' -declare module '../dist/src/index' { +declare module '../dist/src' { export interface MapStoresCustomization { - // this is the only one that can be applied to work with other tests suffix: 'Store' } + + export interface PiniaCustomProperties { + $actions: Array + } + + export interface DefineStoreOptions { + debounce?: { + // Record + [k in keyof A]?: number + } + } } -const useCounter = defineStore({ - id: 'counter', - state: () => ({ n: 0 }), +const pinia = createPinia() + +pinia.use((context) => { + expectType(context.options.id) + expectType(context.store.$id) + + return { + $actions: Object.keys(context.options.actions || {}), + } +}) + +const useStore = defineStore({ + id: 'main', + actions: { + one() {}, + two() { + this.one() + }, + three() { + this.two() + }, + }, + + debounce: { + one: 200, + two: 300, + // three: 100 + }, }) -type CounterStore = ReturnType +type Procedure = (...args: any[]) => any -const computedStores = mapStores(useCounter) +function debounce(fn: F, time = 200) { + return fn +} expectType<{ - counterStore: () => CounterStore -}>(computedStores) + mainStore: () => ReturnType +}>(mapStores(useStore)) + +pinia.use(({ options, store }) => { + if (options.debounce) { + return Object.keys(options.debounce).reduce((debouncedActions, action) => { + debouncedActions[action] = debounce( + store[action], + options.debounce![action as keyof typeof options['actions']] + ) + return debouncedActions + }, {} as Record any>) + } +}) diff --git a/test-dts/deprecated.test-d.ts b/test-dts/deprecated.test-d.ts index 5acd8258..43e40b7b 100644 --- a/test-dts/deprecated.test-d.ts +++ b/test-dts/deprecated.test-d.ts @@ -4,9 +4,7 @@ const useDeprecated = createStore({ id: 'name', state: () => ({ a: 'on' as 'on' | 'off', nested: { counter: 0 } }), getters: { - upper() { - return this.a.toUpperCase() - }, + upper: (state) => state.a.toUpperCase(), }, }) diff --git a/test-dts/mapHelpers.test-d.ts b/test-dts/mapHelpers.test-d.ts index 8b779992..72dbd803 100644 --- a/test-dts/mapHelpers.test-d.ts +++ b/test-dts/mapHelpers.test-d.ts @@ -11,9 +11,7 @@ const useStore = defineStore({ id: 'name', state: () => ({ a: 'on' as 'on' | 'off', nested: { counter: 0 } }), getters: { - upper() { - return this.a.toUpperCase() - }, + upper: (state) => state.a.toUpperCase(), }, actions: { toggleA() { diff --git a/test-dts/plugins.test-d.ts b/test-dts/plugins.test-d.ts new file mode 100644 index 00000000..ecd3ccc6 --- /dev/null +++ b/test-dts/plugins.test-d.ts @@ -0,0 +1,23 @@ +import { + expectType, + createPinia, + GenericStore, + Pinia, + StateTree, + DefineStoreOptions, +} from '.' + +const pinia = createPinia() + +pinia.use(({ store, options, pinia }) => { + expectType(store) + expectType(pinia) + expectType< + DefineStoreOptions< + string, + StateTree, + Record, + Record + > + >(options) +}) diff --git a/test-dts/store.test-d.ts b/test-dts/store.test-d.ts index f068a4b6..e3174984 100644 --- a/test-dts/store.test-d.ts +++ b/test-dts/store.test-d.ts @@ -1,31 +1,51 @@ -import { GenericStore } from 'dist/src/types' -import { defineStore, expectType, createPinia } from './' - -const pinia = createPinia() - -pinia.use(({ store }) => { - expectType(store) -}) +import { defineStore, expectType } from './' const useStore = defineStore({ id: 'name', state: () => ({ a: 'on' as 'on' | 'off', nested: { counter: 0 } }), getters: { - upper() { - return this.a.toUpperCase() + upper: (state) => { + expectType<'on' | 'off'>(state.a) + return state.a.toUpperCase() as 'ON' | 'OFF' + }, + upperThis(): 'ON' | 'OFF' { + expectType<'on' | 'off'>(this.a) + return this.a.toUpperCase() as 'ON' | 'OFF' + }, + other(): false { + expectType(this.upper) + return false + }, + + doubleCounter: (state) => { + expectType(state.nested.counter) + return state.nested.counter * 2 + }, + }, + actions: { + doStuff() { + expectType(this.upper) + expectType(this.other) + }, + otherOne() { + expectType<() => void>(this.doStuff) }, }, }) -const store = useStore() +let store = useStore() expectType<{ a: 'on' | 'off' }>(store.$state) expectType(store.nested.counter) expectType<'on' | 'off'>(store.a) +expectType<'ON' | 'OFF'>(store.upper) // @ts-expect-error store.nonExistant +// @ts-expect-error +store.upper = 'thing' + // @ts-expect-error store.nonExistant.stuff