Skip to content

Commit

Permalink
fix(types): allow writable getters
Browse files Browse the repository at this point in the history
Fix #2767
  • Loading branch information
posva committed Sep 30, 2024
1 parent 855510a commit 94f5a63
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 6 deletions.
43 changes: 43 additions & 0 deletions packages/pinia/__tests__/getters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,49 @@ describe('Getters', () => {
expect(store.upperCaseName).toBe('ED')
})

it('can use getters with setters', () => {
const useStore = defineStore('main', () => {
const name = ref('Eduardo')
const upperCaseName = computed({
get() {
return name.value.toUpperCase()
},
set(value: string) {
store.name = value.toLowerCase()
},
})
return { name, upperCaseName }
})

const store = useStore()
expect(store.upperCaseName).toBe('EDUARDO')
store.upperCaseName = 'ED'
expect(store.name).toBe('ed')
})

it('can use getters with setters with different types', () => {
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 store = useStore()
store.double = 4
expect(store.n).toBe(2)
// @ts-expect-error: still not doable
store.double = '6'
expect(store.n).toBe(3)
})

describe('cross used stores', () => {
const useA = defineStore('a', () => {
const B = useB()
Expand Down
2 changes: 2 additions & 0 deletions packages/pinia/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {
DefineStoreOptionsInPlugin,
StoreGeneric,
_StoreWithGetters,
_StoreWithGetters_Readonly,
_StoreWithGetters_Writable,
_ExtractActionsFromSetupStore,
_ExtractGettersFromSetupStore,
_ExtractStateFromSetupStore,
Expand Down
28 changes: 24 additions & 4 deletions packages/pinia/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
Ref,
UnwrapRef,
WatchOptions,
WritableComputedRef,
} from 'vue-demi'
import { Pinia } from './rootStore'

Expand Down Expand Up @@ -451,10 +452,29 @@ export type _StoreWithActions<A> = {
* Store augmented with getters. For internal usage only.
* For internal use **only**
*/
export type _StoreWithGetters<G> = {
readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R
? R
: UnwrapRef<G[k]>
export type _StoreWithGetters<G> = _StoreWithGetters_Readonly<G> &
_StoreWithGetters_Writable<G>

/**
* Store augmented with readonly getters. For internal usage **only**.
*/
export type _StoreWithGetters_Readonly<G> = {
readonly [K in keyof G as G[K] extends (...args: any[]) => any
? K
: ComputedRef extends G[K]
? K
: never]: G[K] extends (...args: any[]) => infer R ? R : UnwrapRef<G[K]>
}

/**
* Store augmented with writable getters. For internal usage **only**.
*/
export type _StoreWithGetters_Writable<G> = {
[K in keyof G as G[K] extends WritableComputedRef<any>
? K
: // NOTE: there is still no way to have a different type for a setter and a getter in TS with dynamic keys
// https://github.com/microsoft/TypeScript/issues/43826
never]: G[K] extends WritableComputedRef<infer R, infer _S> ? R : never
}

/**
Expand Down
34 changes: 32 additions & 2 deletions packages/pinia/test-dts/store.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StoreGeneric, acceptHMRUpdate, defineStore, expectType } from './'
import { UnwrapRef, watch } from 'vue'
import { computed, ref, UnwrapRef, watch } from 'vue'

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

export const useSyncValueToStore = <
TStore extends StoreGeneric,
TKey extends keyof TStore['$state']
TKey extends keyof TStore['$state'],
>(
propGetter: () => TStore[TKey],
store: TStore,
Expand Down Expand Up @@ -282,3 +282,33 @@ useSyncValueToStore(() => 2, genericStore, 'myState')
// @ts-expect-error: this type is known so it should yield an error
useSyncValueToStore(() => false, genericStore, 'myState')
useSyncValueToStore(() => 2, genericStore, 'random')

const writableComputedStore = defineStore('computed-writable', () => {
const fruitsBasket = ref(['banana', 'apple', 'banana', 'orange'])
const bananasAmount = computed<number>({
get: () => fruitsBasket.value.filter((fruit) => fruit === 'banana').length,
set: (newAmount) => {
fruitsBasket.value = fruitsBasket.value.filter(
(fruit) => fruit !== 'banana'
)
fruitsBasket.value.push(...Array(newAmount).fill('banana'))
},
})
const bananas = computed({
get: () => fruitsBasket.value.filter((fruit) => fruit === 'banana'),
set: (newFruit: string) =>
(fruitsBasket.value = fruitsBasket.value.map((fruit) =>
fruit === 'banana' ? newFruit : fruit
)),
})
bananas.value = 'hello' // TS ok
return { fruitsBasket, bananas, bananasAmount }
})()

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

0 comments on commit 94f5a63

Please sign in to comment.