Skip to content

Commit

Permalink
fix(types): allow writable getters with storeToRefs
Browse files Browse the repository at this point in the history
Fix #2767
  • Loading branch information
posva committed Oct 1, 2024
1 parent 09935e8 commit b464a1f
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 6 deletions.
19 changes: 19 additions & 0 deletions packages/pinia/__tests__/storeToRefs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,25 @@ describe('storeToRefs', () => {
).toEqual(objectOfRefs({ n: 0, pluginN: 20 }))
})

it('preserve setters in getters', () => {
const useStore = defineStore('main', () => {
const n = ref(0)
const double = computed({
get() {
return n.value * 2
},
set(value: string | number) {
n.value =
(typeof value === 'string' ? parseInt(value) || 0 : value) / 2
},
})
return { n, double }
})
const refs = storeToRefs(useStore())
refs.double.value = 4
expect(refs.n.value).toBe(2)
})

tds(() => {
const store1 = defineStore('a', () => {
const n = ref(0)
Expand Down
29 changes: 25 additions & 4 deletions packages/pinia/src/storeToRefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import {
isReactive,
isRef,
isVue2,
Ref,
toRaw,
ToRef,
toRef,
ToRefs,
toRefs,
WritableComputedRef,
} from 'vue-demi'
import { StoreGetters, StoreState } from './store'
import type {
Expand All @@ -21,8 +21,29 @@ import type {
StoreGeneric,
} from './types'

type ToComputedRefs<T> = {
[K in keyof T]: ToRef<T[K]> extends Ref ? ComputedRef<T[K]> : ToRef<T[K]>
/**
* Internal utility type
*/
type _IfEquals<X, Y, A = true, B = false> =
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B

/**
* Internal utility type
*/
type _IsReadonly<T, K extends keyof T> = _IfEquals<
{ [P in K]: T[P] },
{ -readonly [P in K]: T[P] },
false, // Property is not readonly if they are the same
true // Property is readonly if they differ
>

/**
* Extracts the getters of a store while keeping writable and readonly properties. **Internal type DO NOT USE**.
*/
type _ToComputedRefs<SS> = {
[K in keyof SS]: true extends _IsReadonly<SS, K>
? ComputedRef<SS[K]>
: WritableComputedRef<SS[K]>
}

/**
Expand All @@ -49,7 +70,7 @@ type _ToStateRefs<SS> =
*/
export type StoreToRefs<SS extends StoreGeneric> = _ToStateRefs<SS> &
ToRefs<PiniaCustomStateProperties<StoreState<SS>>> &
ToComputedRefs<StoreGetters<SS>>
_ToComputedRefs<StoreGetters<SS>>

/**
* Creates an object of references with all the state, getters, and plugin-added
Expand Down
13 changes: 13 additions & 0 deletions packages/pinia/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,3 +727,16 @@ export interface DefineStoreOptionsInPlugin<
*/
actions: A
}

/**
* Utility type. For internal use **only**
*/
export interface _Empty {}

/**
* Merges type objects for better readability in the code.
* Utility type. For internal use **only**
*/
export type _Simplify<T> = _Empty extends T
? _Empty
: { [key in keyof T]: T[key] } & {}
20 changes: 18 additions & 2 deletions packages/pinia/test-dts/store.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { StoreGeneric, acceptHMRUpdate, defineStore, expectType } from './'
import {
StoreGeneric,
acceptHMRUpdate,
defineStore,
expectType,
storeToRefs,
} from './'
import { computed, ref, UnwrapRef, watch } from 'vue'

const useStore = defineStore({
Expand Down Expand Up @@ -285,6 +291,7 @@ useSyncValueToStore(() => 2, genericStore, 'random')

const writableComputedStore = defineStore('computed-writable', () => {
const fruitsBasket = ref(['banana', 'apple', 'banana', 'orange'])
const total = computed(() => fruitsBasket.value.length)
const bananasAmount = computed<number>({
get: () => fruitsBasket.value.filter((fruit) => fruit === 'banana').length,
set: (newAmount) => {
Expand All @@ -302,13 +309,22 @@ const writableComputedStore = defineStore('computed-writable', () => {
)),
})
bananas.value = 'hello' // TS ok
return { fruitsBasket, bananas, bananasAmount }
return { fruitsBasket, bananas, bananasAmount, total }
})()

expectType<number>(writableComputedStore.bananasAmount)
// should allow writing to it
writableComputedStore.bananasAmount = 0
// @ts-expect-error: this one is readonly
writableComputedStore.total = 0
expectType<string[]>(writableComputedStore.bananas)
// should allow setting a different type
// @ts-expect-error: still not doable
writableComputedStore.bananas = 'hello'

const refs = storeToRefs(writableComputedStore)
expectType<string[]>(refs.bananas.value)
expectType<number>(refs.bananasAmount.value)
refs.bananasAmount.value = 0
// @ts-expect-error: this one is readonly
refs.total.value = 0

0 comments on commit b464a1f

Please sign in to comment.