Skip to content

Commit b464a1f

Browse files
committed
fix(types): allow writable getters with storeToRefs
Fix #2767
1 parent 09935e8 commit b464a1f

File tree

4 files changed

+75
-6
lines changed

4 files changed

+75
-6
lines changed

packages/pinia/__tests__/storeToRefs.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,25 @@ describe('storeToRefs', () => {
151151
).toEqual(objectOfRefs({ n: 0, pluginN: 20 }))
152152
})
153153

154+
it('preserve setters in getters', () => {
155+
const useStore = defineStore('main', () => {
156+
const n = ref(0)
157+
const double = computed({
158+
get() {
159+
return n.value * 2
160+
},
161+
set(value: string | number) {
162+
n.value =
163+
(typeof value === 'string' ? parseInt(value) || 0 : value) / 2
164+
},
165+
})
166+
return { n, double }
167+
})
168+
const refs = storeToRefs(useStore())
169+
refs.double.value = 4
170+
expect(refs.n.value).toBe(2)
171+
})
172+
154173
tds(() => {
155174
const store1 = defineStore('a', () => {
156175
const n = ref(0)

packages/pinia/src/storeToRefs.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import {
33
isReactive,
44
isRef,
55
isVue2,
6-
Ref,
76
toRaw,
87
ToRef,
98
toRef,
109
ToRefs,
1110
toRefs,
11+
WritableComputedRef,
1212
} from 'vue-demi'
1313
import { StoreGetters, StoreState } from './store'
1414
import type {
@@ -21,8 +21,29 @@ import type {
2121
StoreGeneric,
2222
} from './types'
2323

24-
type ToComputedRefs<T> = {
25-
[K in keyof T]: ToRef<T[K]> extends Ref ? ComputedRef<T[K]> : ToRef<T[K]>
24+
/**
25+
* Internal utility type
26+
*/
27+
type _IfEquals<X, Y, A = true, B = false> =
28+
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B
29+
30+
/**
31+
* Internal utility type
32+
*/
33+
type _IsReadonly<T, K extends keyof T> = _IfEquals<
34+
{ [P in K]: T[P] },
35+
{ -readonly [P in K]: T[P] },
36+
false, // Property is not readonly if they are the same
37+
true // Property is readonly if they differ
38+
>
39+
40+
/**
41+
* Extracts the getters of a store while keeping writable and readonly properties. **Internal type DO NOT USE**.
42+
*/
43+
type _ToComputedRefs<SS> = {
44+
[K in keyof SS]: true extends _IsReadonly<SS, K>
45+
? ComputedRef<SS[K]>
46+
: WritableComputedRef<SS[K]>
2647
}
2748

2849
/**
@@ -49,7 +70,7 @@ type _ToStateRefs<SS> =
4970
*/
5071
export type StoreToRefs<SS extends StoreGeneric> = _ToStateRefs<SS> &
5172
ToRefs<PiniaCustomStateProperties<StoreState<SS>>> &
52-
ToComputedRefs<StoreGetters<SS>>
73+
_ToComputedRefs<StoreGetters<SS>>
5374

5475
/**
5576
* Creates an object of references with all the state, getters, and plugin-added

packages/pinia/src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,3 +727,16 @@ export interface DefineStoreOptionsInPlugin<
727727
*/
728728
actions: A
729729
}
730+
731+
/**
732+
* Utility type. For internal use **only**
733+
*/
734+
export interface _Empty {}
735+
736+
/**
737+
* Merges type objects for better readability in the code.
738+
* Utility type. For internal use **only**
739+
*/
740+
export type _Simplify<T> = _Empty extends T
741+
? _Empty
742+
: { [key in keyof T]: T[key] } & {}

packages/pinia/test-dts/store.test-d.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { StoreGeneric, acceptHMRUpdate, defineStore, expectType } from './'
1+
import {
2+
StoreGeneric,
3+
acceptHMRUpdate,
4+
defineStore,
5+
expectType,
6+
storeToRefs,
7+
} from './'
28
import { computed, ref, UnwrapRef, watch } from 'vue'
39

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

286292
const writableComputedStore = defineStore('computed-writable', () => {
287293
const fruitsBasket = ref(['banana', 'apple', 'banana', 'orange'])
294+
const total = computed(() => fruitsBasket.value.length)
288295
const bananasAmount = computed<number>({
289296
get: () => fruitsBasket.value.filter((fruit) => fruit === 'banana').length,
290297
set: (newAmount) => {
@@ -302,13 +309,22 @@ const writableComputedStore = defineStore('computed-writable', () => {
302309
)),
303310
})
304311
bananas.value = 'hello' // TS ok
305-
return { fruitsBasket, bananas, bananasAmount }
312+
return { fruitsBasket, bananas, bananasAmount, total }
306313
})()
307314

308315
expectType<number>(writableComputedStore.bananasAmount)
309316
// should allow writing to it
310317
writableComputedStore.bananasAmount = 0
318+
// @ts-expect-error: this one is readonly
319+
writableComputedStore.total = 0
311320
expectType<string[]>(writableComputedStore.bananas)
312321
// should allow setting a different type
313322
// @ts-expect-error: still not doable
314323
writableComputedStore.bananas = 'hello'
324+
325+
const refs = storeToRefs(writableComputedStore)
326+
expectType<string[]>(refs.bananas.value)
327+
expectType<number>(refs.bananasAmount.value)
328+
refs.bananasAmount.value = 0
329+
// @ts-expect-error: this one is readonly
330+
refs.total.value = 0

0 commit comments

Comments
 (0)