Skip to content

Commit 94f5a63

Browse files
committed
fix(types): allow writable getters
Fix #2767
1 parent 855510a commit 94f5a63

File tree

4 files changed

+101
-6
lines changed

4 files changed

+101
-6
lines changed

packages/pinia/__tests__/getters.spec.ts

+43
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,49 @@ describe('Getters', () => {
106106
expect(store.upperCaseName).toBe('ED')
107107
})
108108

109+
it('can use getters with setters', () => {
110+
const useStore = defineStore('main', () => {
111+
const name = ref('Eduardo')
112+
const upperCaseName = computed({
113+
get() {
114+
return name.value.toUpperCase()
115+
},
116+
set(value: string) {
117+
store.name = value.toLowerCase()
118+
},
119+
})
120+
return { name, upperCaseName }
121+
})
122+
123+
const store = useStore()
124+
expect(store.upperCaseName).toBe('EDUARDO')
125+
store.upperCaseName = 'ED'
126+
expect(store.name).toBe('ed')
127+
})
128+
129+
it('can use getters with setters with different types', () => {
130+
const useStore = defineStore('main', () => {
131+
const n = ref(0)
132+
const double = computed({
133+
get() {
134+
return n.value * 2
135+
},
136+
set(value: string | number) {
137+
n.value =
138+
(typeof value === 'string' ? parseInt(value) || 0 : value) / 2
139+
},
140+
})
141+
return { n, double }
142+
})
143+
144+
const store = useStore()
145+
store.double = 4
146+
expect(store.n).toBe(2)
147+
// @ts-expect-error: still not doable
148+
store.double = '6'
149+
expect(store.n).toBe(3)
150+
})
151+
109152
describe('cross used stores', () => {
110153
const useA = defineStore('a', () => {
111154
const B = useB()

packages/pinia/src/store.ts

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import {
4242
DefineStoreOptionsInPlugin,
4343
StoreGeneric,
4444
_StoreWithGetters,
45+
_StoreWithGetters_Readonly,
46+
_StoreWithGetters_Writable,
4547
_ExtractActionsFromSetupStore,
4648
_ExtractGettersFromSetupStore,
4749
_ExtractStateFromSetupStore,

packages/pinia/src/types.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
Ref,
55
UnwrapRef,
66
WatchOptions,
7+
WritableComputedRef,
78
} from 'vue-demi'
89
import { Pinia } from './rootStore'
910

@@ -451,10 +452,29 @@ export type _StoreWithActions<A> = {
451452
* Store augmented with getters. For internal usage only.
452453
* For internal use **only**
453454
*/
454-
export type _StoreWithGetters<G> = {
455-
readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R
456-
? R
457-
: UnwrapRef<G[k]>
455+
export type _StoreWithGetters<G> = _StoreWithGetters_Readonly<G> &
456+
_StoreWithGetters_Writable<G>
457+
458+
/**
459+
* Store augmented with readonly getters. For internal usage **only**.
460+
*/
461+
export type _StoreWithGetters_Readonly<G> = {
462+
readonly [K in keyof G as G[K] extends (...args: any[]) => any
463+
? K
464+
: ComputedRef extends G[K]
465+
? K
466+
: never]: G[K] extends (...args: any[]) => infer R ? R : UnwrapRef<G[K]>
467+
}
468+
469+
/**
470+
* Store augmented with writable getters. For internal usage **only**.
471+
*/
472+
export type _StoreWithGetters_Writable<G> = {
473+
[K in keyof G as G[K] extends WritableComputedRef<any>
474+
? K
475+
: // NOTE: there is still no way to have a different type for a setter and a getter in TS with dynamic keys
476+
// https://github.com/microsoft/TypeScript/issues/43826
477+
never]: G[K] extends WritableComputedRef<infer R, infer _S> ? R : never
458478
}
459479

460480
/**

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

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { StoreGeneric, acceptHMRUpdate, defineStore, expectType } from './'
2-
import { UnwrapRef, watch } from 'vue'
2+
import { computed, ref, UnwrapRef, watch } from 'vue'
33

44
const useStore = defineStore({
55
id: 'name',
@@ -236,7 +236,7 @@ function takeStore<TStore extends StoreGeneric>(store: TStore): TStore['$id'] {
236236

237237
export const useSyncValueToStore = <
238238
TStore extends StoreGeneric,
239-
TKey extends keyof TStore['$state']
239+
TKey extends keyof TStore['$state'],
240240
>(
241241
propGetter: () => TStore[TKey],
242242
store: TStore,
@@ -282,3 +282,33 @@ useSyncValueToStore(() => 2, genericStore, 'myState')
282282
// @ts-expect-error: this type is known so it should yield an error
283283
useSyncValueToStore(() => false, genericStore, 'myState')
284284
useSyncValueToStore(() => 2, genericStore, 'random')
285+
286+
const writableComputedStore = defineStore('computed-writable', () => {
287+
const fruitsBasket = ref(['banana', 'apple', 'banana', 'orange'])
288+
const bananasAmount = computed<number>({
289+
get: () => fruitsBasket.value.filter((fruit) => fruit === 'banana').length,
290+
set: (newAmount) => {
291+
fruitsBasket.value = fruitsBasket.value.filter(
292+
(fruit) => fruit !== 'banana'
293+
)
294+
fruitsBasket.value.push(...Array(newAmount).fill('banana'))
295+
},
296+
})
297+
const bananas = computed({
298+
get: () => fruitsBasket.value.filter((fruit) => fruit === 'banana'),
299+
set: (newFruit: string) =>
300+
(fruitsBasket.value = fruitsBasket.value.map((fruit) =>
301+
fruit === 'banana' ? newFruit : fruit
302+
)),
303+
})
304+
bananas.value = 'hello' // TS ok
305+
return { fruitsBasket, bananas, bananasAmount }
306+
})()
307+
308+
expectType<number>(writableComputedStore.bananasAmount)
309+
// should allow writing to it
310+
writableComputedStore.bananasAmount = 0
311+
expectType<string[]>(writableComputedStore.bananas)
312+
// should allow setting a different type
313+
// @ts-expect-error: still not doable
314+
writableComputedStore.bananas = 'hello'

0 commit comments

Comments
 (0)