Skip to content

Commit

Permalink
fix(types): forbid non existants keys on store
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed May 15, 2021
1 parent 17fcbca commit e747cba
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/devtools/formatting.ts
Expand Up @@ -32,6 +32,7 @@ export function formatStoreForInspectorState(
editable: false,
key: 'getters',
value: store._getters.reduce((getters, key) => {
// @ts-expect-error
getters[key] = store[key]
return getters
}, {} as GettersTree<StateTree>),
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -11,6 +11,7 @@ export type {
StoreDefinition,
StoreWithGetters,
GettersTree,
ActionsTree,
_Method,
StoreWithActions,
StoreWithState,
Expand Down
38 changes: 27 additions & 11 deletions src/mapHelpers.ts
Expand Up @@ -139,7 +139,14 @@ export function mapStores<Stores extends any[]>(
* @internal
*/
export type _MapStateReturn<S extends StateTree, G extends GettersTree<S>> = {
[key in keyof S | keyof G]: () => Store<string, S, G, {}>[key]
// [key in keyof S | keyof G]: () => key extends keyof S
// ? S[key]
// : key extends keyof G
// ? G[key]
// : never
[key in keyof S | keyof G]: () => key extends keyof Store<string, S, G, {}>
? Store<string, S, G, {}>[key]
: never
}

/**
Expand All @@ -157,7 +164,7 @@ export type _MapStateObjectReturn<
> = {
[key in keyof T]: () => T[key] extends (store: Store) => infer R
? R
: T[key] extends keyof S | keyof G
: T[key] extends keyof Store<Id, S, G, A>
? Store<Id, S, G, A>[T[key]]
: never
}
Expand Down Expand Up @@ -267,6 +274,7 @@ export function mapState<
return Array.isArray(keysOrMapper)
? keysOrMapper.reduce((reduced, key) => {
reduced[key] = function (this: ComponentPublicInstance) {
// @ts-expect-error
return getCachedStore(this, useStore)[key]
} as () => any
return reduced
Expand All @@ -279,7 +287,7 @@ export function mapState<
// function
return typeof storeKey === 'function'
? (storeKey as (store: Store<Id, S, G, A>) => any).call(this, store)
: store[storeKey as keyof S | keyof G]
: store[storeKey as keyof typeof store]
}
return reduced
}, {} as _MapStateObjectReturn<Id, S, G, A, KeyMapper>)
Expand All @@ -295,14 +303,14 @@ export const mapGetters = mapState
* @internal
*/
export type _MapActionsReturn<A> = {
[key in keyof A]: Store<string, StateTree, {}, A>[key]
[key in keyof A]: A[key]
}

/**
* @internal
*/
export type _MapActionsObjectReturn<A, T extends Record<string, keyof A>> = {
[key in keyof T]: Store<string, StateTree, {}, A>[T[key]]
[key in keyof T]: A[T[key]]
}

/**
Expand Down Expand Up @@ -392,21 +400,25 @@ export function mapActions<
): _MapActionsReturn<A> | _MapActionsObjectReturn<A, KeyMapper> {
return Array.isArray(keysOrMapper)
? keysOrMapper.reduce((reduced, key) => {
// @ts-expect-error
reduced[key] = function (
this: ComponentPublicInstance,
...args: any[]
) {
// @ts-expect-error
return (getCachedStore(this, useStore)[key] as _Method)(...args)
} as Store<string, StateTree, {}, A>[keyof A]
}
return reduced
}, {} as _MapActionsReturn<A>)
: Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => {
// @ts-expect-error
reduced[key] = function (
this: ComponentPublicInstance,
...args: any[]
) {
// @ts-expect-error
return getCachedStore(this, useStore)[keysOrMapper[key]](...args)
} as Store<string, StateTree, {}, A>[keyof KeyMapper[]]
}
return reduced
}, {} as _MapActionsObjectReturn<A, KeyMapper>)
}
Expand All @@ -416,8 +428,8 @@ export function mapActions<
*/
export type _MapWritableStateReturn<S extends StateTree> = {
[key in keyof S]: {
get: () => Store<string, S, {}, {}>[key]
set: (value: Store<string, S, {}, {}>[key]) => any
get: () => S[key]
set: (value: S[key]) => any
}
}

Expand All @@ -429,8 +441,8 @@ export type _MapWritableStateObjectReturn<
T extends Record<string, keyof S>
> = {
[key in keyof T]: {
get: () => Store<string, S, {}, {}>[T[key]]
set: (value: Store<string, S, {}, {}>[T[key]]) => any
get: () => S[T[key]]
set: (value: S[T[key]]) => any
}
}

Expand Down Expand Up @@ -492,10 +504,12 @@ export function mapWritableState<
// @ts-ignore
reduced[key] = {
get(this: ComponentPublicInstance) {
// @ts-expect-error
return getCachedStore(this, useStore)[key]
},
set(this: ComponentPublicInstance, value) {
// it's easier to type it here as any
// @ts-expect-error
return (getCachedStore(this, useStore)[key] = value as any)
},
}
Expand All @@ -505,10 +519,12 @@ export function mapWritableState<
// @ts-ignore
reduced[key] = {
get(this: ComponentPublicInstance) {
// @ts-expect-error
return getCachedStore(this, useStore)[keysOrMapper[key]]
},
set(this: ComponentPublicInstance, value) {
// it's easier to type it here as any
// @ts-expect-error
return (getCachedStore(this, useStore)[keysOrMapper[key]] =
value as any)
},
Expand Down
11 changes: 5 additions & 6 deletions src/store.ts
Expand Up @@ -425,8 +425,8 @@ export function defineStore<
storeAndDescriptor[0],
storeAndDescriptor[1],
id,
getters as GettersTree<S> | undefined,
actions as A | undefined,
getters,
actions,
options
)

Expand All @@ -437,20 +437,19 @@ export function defineStore<
}
} else {
store =
// null avoids the warning for not found injection key
(currentInstance && inject(storeAndDescriptor[2], null)) ||
buildStoreToUse<
Id,
S,
G,
// @ts-expect-error: A without extends
// @ts-expect-error: cannot extends ActionsTree
A
>(
storeAndDescriptor[0],
storeAndDescriptor[1],
id,
getters as GettersTree<S> | undefined,
actions as A | undefined,
getters,
actions,
options
)
}
Expand Down
32 changes: 22 additions & 10 deletions src/types.ts
Expand Up @@ -231,7 +231,8 @@ export interface StoreWithState<
/**
* State of the Store. Setting it will replace the whole state.
*/
$state: UnwrapRef<S> & PiniaCustomStateProperties<S>
$state: (StateTree extends S ? {} : UnwrapRef<S>) &
PiniaCustomStateProperties<S>

/**
* Private property defining the pinia the store is attached to.
Expand Down Expand Up @@ -377,12 +378,29 @@ export type Store<
// has the actions without the context (this) for typings
A /* extends ActionsTree */ = ActionsTree
> = StoreWithState<Id, S, G, A> &
UnwrapRef<S> &
StoreWithGetters<G> &
StoreWithActions<A> &
(StateTree extends S ? {} : UnwrapRef<S>) &
(GettersTree<S> extends G ? {} : StoreWithGetters<G>) &
(ActionsTree extends A ? {} : StoreWithActions<A>) &
PiniaCustomProperties<Id, S, G, A> &
PiniaCustomStateProperties<S>

/**
* Generic version of Store. Doesn't fail on access with strings
*/
export type GenericStore = StoreWithState<
string,
StateTree,
GettersTree<StateTree>,
ActionsTree
> &
PiniaCustomProperties<
string,
StateTree,
GettersTree<StateTree>,
ActionsTree
> &
PiniaCustomStateProperties<StateTree>

/**
* Return type of `defineStore()`. Function that allows instantiating a store.
*/
Expand All @@ -405,12 +423,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()`
*/
Expand Down
5 changes: 2 additions & 3 deletions test-dts/customizations.test-d.ts
@@ -1,6 +1,5 @@
import { mapStores } from 'dist/pinia'
import { expectType, createPinia, defineStore, mapStores } from './'
import { App } from 'vue'
import { expectType, createPinia, defineStore } from '.'

declare module '../dist/pinia' {
export interface MapStoresCustomization {
Expand Down Expand Up @@ -40,7 +39,6 @@ pinia.use((context) => {

const useStore = defineStore({
id: 'main',
state: () => ({}),
actions: {
one() {},
two() {
Expand Down Expand Up @@ -74,6 +72,7 @@ pinia.use(({ options, store }) => {
if (options.debounce) {
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
debouncedActions[action] = debounce(
// @ts-expect-error: cannot be inferred
store[action],
options.debounce![action as keyof typeof options['actions']]
)
Expand Down
2 changes: 1 addition & 1 deletion test-dts/mapHelpers.test-d.ts
Expand Up @@ -5,7 +5,7 @@ import {
mapActions,
mapState,
mapWritableState,
} from '.'
} from './'

const useStore = defineStore({
id: 'name',
Expand Down
2 changes: 1 addition & 1 deletion test-dts/plugins.test-d.ts
Expand Up @@ -6,7 +6,7 @@ import {
Pinia,
StateTree,
DefineStoreOptions,
} from '.'
} from './'

const pinia = createPinia()

Expand Down
64 changes: 63 additions & 1 deletion test-dts/store.test-d.ts
Expand Up @@ -33,7 +33,7 @@ const useStore = defineStore({
},
})

let store = useStore()
const store = useStore()

expectType<{ a: 'on' | 'off' }>(store.$state)
expectType<number>(store.nested.counter)
Expand All @@ -56,3 +56,65 @@ store.$patch(() => {
// return earlier
return
})

const useNoSAG = defineStore({
id: 'noSAG',
})
const useNoAG = defineStore({
id: 'noAG',
state: () => ({}),
})
const useNoSG = defineStore({
id: 'noAG',
actions: {},
})
const useNoSA = defineStore({
id: 'noAG',
getters: {},
})
const useNoS = defineStore({
id: 'noAG',
actions: {},
getters: {},
})
const useNoA = defineStore({
id: 'noAG',
state: () => ({}),
getters: {},
})
const useNoG = defineStore({
id: 'noAG',
state: () => ({}),
actions: {},
})

const noSAG = useNoSAG()
const noSA = useNoSA()
const noAG = useNoAG()
const noSG = useNoSG()
const noS = useNoS()
const noA = useNoA()
const noG = useNoG()

// @ts-expect-error
store.notExisting

// @ts-expect-error
noSAG.notExisting
// @ts-expect-error
noSAG.$state.hey

// @ts-expect-error
noSA.notExisting
// @ts-expect-error
noSA.notExisting
// @ts-expect-error
noAG.notExisting
// @ts-expect-error
noSG.notExisting
// @ts-expect-error
noS.notExisting
// @ts-expect-error
noA.notExisting
// @ts-expect-error
noG.notExisting

0 comments on commit e747cba

Please sign in to comment.