From 5745ce86760ff3100fe1860f04b5d478d41db570 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 12 May 2021 18:44:12 +0200 Subject: [PATCH] feat: allow strict option Close #58 --- __tests__/strictMode.spec.ts | 31 +++++++++++++++++++++++++++ src/rootStore.ts | 5 +++-- src/store.ts | 26 ++++++++++++++--------- src/types.ts | 35 ++++++++++++++++++++++++------- test-dts/customizations.test-d.ts | 4 ++-- test-dts/plugins.test-d.ts | 5 ++++- 6 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 __tests__/strictMode.spec.ts diff --git a/__tests__/strictMode.spec.ts b/__tests__/strictMode.spec.ts new file mode 100644 index 0000000000..ec3395d70b --- /dev/null +++ b/__tests__/strictMode.spec.ts @@ -0,0 +1,31 @@ +import { createPinia, defineStore, setActivePinia } from '../src' + +describe('Strict mode', () => { + const useStore = defineStore({ + id: 'main', + strict: true, + state: () => ({ + a: true, + nested: { + foo: 'foo', + a: { b: 'string' }, + }, + }), + }) + + it('cannot change the state directly', () => { + setActivePinia(createPinia()) + const store = useStore() + // @ts-expect-error + store.a = false + // @ts-expect-error + store.nested.foo = 'bar' + + // TODO: should direct $state be allowed? + // this could be an escape hatch if we want one + store.$state.a = false + + store.$patch({ a: false }) + store.$patch({ nested: { foo: 'bar' } }) + }) +}) diff --git a/src/rootStore.ts b/src/rootStore.ts index e991852bbe..c9aef4d946 100644 --- a/src/rootStore.ts +++ b/src/rootStore.ts @@ -120,7 +120,8 @@ export interface PiniaPluginContext< Id extends string = string, S extends StateTree = StateTree, G extends GettersTree = GettersTree, - A /* extends ActionsTree */ = ActionsTree + A /* extends ActionsTree */ = ActionsTree, + Strict extends boolean = false > { /** * pinia instance. @@ -140,7 +141,7 @@ export interface PiniaPluginContext< /** * Current store being extended. */ - options: DefineStoreOptions + options: DefineStoreOptions } /** diff --git a/src/store.ts b/src/store.ts index f927028fbe..f65ab1b663 100644 --- a/src/store.ts +++ b/src/store.ts @@ -276,14 +276,15 @@ function buildStoreToUse< Id extends string, S extends StateTree, G extends GettersTree, - A extends ActionsTree + A extends ActionsTree, + Strict extends boolean >( partialStore: StoreWithState, descriptor: StateDescriptor, $id: Id, getters: G = {} as G, actions: A = {} as A, - options: DefineStoreOptions + options: DefineStoreOptions ) { const pinia = getActivePinia() @@ -337,7 +338,7 @@ function buildStoreToUse< } as StoreWithActions[typeof actionName] } - const store: Store = reactive( + const store: Store = reactive( assign( {}, partialStore, @@ -346,7 +347,7 @@ function buildStoreToUse< computedGetters, wrappedActions ) - ) as Store + ) as Store // use this instead of a computed with setter to be able to create it anywhere // without linking the computed lifespan to wherever the store is first @@ -375,11 +376,14 @@ export function defineStore< S extends StateTree, G extends GettersTree, // cannot extends ActionsTree because we loose the typings - A /* extends ActionsTree */ ->(options: DefineStoreOptions): StoreDefinition { + A /* extends ActionsTree */, + Strict extends boolean +>( + options: DefineStoreOptions +): StoreDefinition { const { id, state, getters, actions } = options - function useStore(pinia?: Pinia | null): Store { + function useStore(pinia?: Pinia | null): Store { const hasInstance = getCurrentInstance() // only run provide when pinia hasn't been manually passed const shouldProvide = hasInstance && !pinia @@ -395,7 +399,7 @@ export function defineStore< | [ StoreWithState, StateDescriptor, - InjectionKey> + InjectionKey> ] | undefined if (!storeAndDescriptor) { @@ -409,7 +413,8 @@ export function defineStore< S, G, // @ts-expect-error: A without extends - A + A, + Strict >( storeAndDescriptor[0], storeAndDescriptor[1], @@ -436,7 +441,8 @@ export function defineStore< S, G, // @ts-expect-error: A without extends - A + A, + Strict >( storeAndDescriptor[0], storeAndDescriptor[1], diff --git a/src/types.ts b/src/types.ts index 8aaed3616f..bec604852f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,8 +26,15 @@ export function isPlainObject( ) } +/** + * @internal + */ export type DeepPartial = { [K in keyof T]?: DeepPartial } -// type DeepReadonly = { readonly [P in keyof T]: DeepReadonly } + +/** + * @internal + */ +export type DeepReadonly = { readonly [P in keyof T]: DeepReadonly } /** * Possible types for SubscriptionCallback @@ -142,6 +149,7 @@ export interface StoreWithState< S extends StateTree, G extends GettersTree = GettersTree, A /* extends ActionsTree */ = ActionsTree + // Strict extends boolean = false > { /** * Unique identifier of the store @@ -300,9 +308,10 @@ export type Store< S extends StateTree = StateTree, G extends GettersTree = GettersTree, // has the actions without the context (this) for typings - A /* extends ActionsTree */ = ActionsTree + A /* extends ActionsTree */ = ActionsTree, + Strict extends boolean = false > = StoreWithState & - UnwrapRef & + (false extends Strict ? UnwrapRef : DeepReadonly>) & StoreWithGetters & StoreWithActions & PiniaCustomProperties @@ -314,14 +323,15 @@ export interface StoreDefinition< Id extends string = string, S extends StateTree = StateTree, G extends GettersTree = GettersTree, - A /* extends ActionsTree */ = ActionsTree + A /* extends ActionsTree */ = ActionsTree, + Strict extends boolean = false > { /** * Returns a store, creates it if necessary. * * @param pinia - Pinia instance to retrieve the store */ - (pinia?: Pinia | null | undefined): Store + (pinia?: Pinia | null | undefined): Store /** * Id of the store. Used by map helpers. @@ -342,7 +352,8 @@ export interface PiniaCustomProperties< Id extends string = string, S extends StateTree = StateTree, G extends GettersTree = GettersTree, - A /* extends ActionsTree */ = ActionsTree + A /* extends ActionsTree */ = ActionsTree, + Strict extends boolean = false > {} /** @@ -370,21 +381,29 @@ export interface DefineStoreOptions< Id extends string, S extends StateTree, G extends GettersTree, - A /* extends Record */ + A /* extends Record */, + Strict extends boolean > { /** * Unique string key to identify the store across the application. */ id: Id + + strict?: Strict + /** * Function to create a fresh state. */ state?: () => S + /** * Optional object of getters. */ getters?: G & - ThisType & StoreWithGetters & PiniaCustomProperties> + ThisType< + DeepReadonly> & StoreWithGetters & PiniaCustomProperties + > + /** * Optional object of actions. */ diff --git a/test-dts/customizations.test-d.ts b/test-dts/customizations.test-d.ts index c4aaf6d432..34c7432229 100644 --- a/test-dts/customizations.test-d.ts +++ b/test-dts/customizations.test-d.ts @@ -7,11 +7,11 @@ declare module '../dist/pinia' { suffix: 'Store' } - export interface PiniaCustomProperties { + export interface PiniaCustomProperties { $actions: Array } - export interface DefineStoreOptions { + export interface DefineStoreOptions { debounce?: { // Record [k in keyof A]?: number diff --git a/test-dts/plugins.test-d.ts b/test-dts/plugins.test-d.ts index efb6d226b6..eaf9661907 100644 --- a/test-dts/plugins.test-d.ts +++ b/test-dts/plugins.test-d.ts @@ -3,6 +3,7 @@ import { expectType, createPinia, GenericStore, + Store, Pinia, StateTree, DefineStoreOptions, @@ -12,6 +13,7 @@ const pinia = createPinia() pinia.use(({ store, app, options, pinia }) => { expectType(store) + expectType(store) expectType(pinia) expectType(app) expectType< @@ -19,7 +21,8 @@ pinia.use(({ store, app, options, pinia }) => { string, StateTree, Record, - Record + Record, + boolean > >(options) })