]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
fix(types): allow writable getters
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 30 Sep 2024 14:10:19 +0000 (16:10 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 30 Sep 2024 14:10:19 +0000 (16:10 +0200)
Fix #2767

packages/pinia/__tests__/getters.spec.ts
packages/pinia/src/store.ts
packages/pinia/src/types.ts
packages/pinia/test-dts/store.test-d.ts

index b0c1d9682658e9abea502c3517092ed6a4cbdbcf..2b346031d85987e6e8456899356b2133fd3377f7 100644 (file)
@@ -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()
index 923c4b120bec1869c441acf47e1d5b0d5a9dc7b1..3bd239bdfb1ceb79caa6567f351573353b343b4f 100644 (file)
@@ -42,6 +42,8 @@ import {
   DefineStoreOptionsInPlugin,
   StoreGeneric,
   _StoreWithGetters,
+  _StoreWithGetters_Readonly,
+  _StoreWithGetters_Writable,
   _ExtractActionsFromSetupStore,
   _ExtractGettersFromSetupStore,
   _ExtractStateFromSetupStore,
index f36288a888998b541f77bf630f4af52f90a650a4..3a3f5da96c7a3c9485ea2acdec614da8c6e76f2c 100644 (file)
@@ -4,6 +4,7 @@ import type {
   Ref,
   UnwrapRef,
   WatchOptions,
+  WritableComputedRef,
 } from 'vue-demi'
 import { Pinia } from './rootStore'
 
@@ -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
 }
 
 /**
index 2193f9f8b66d80ecbc57e2161d6299fadd84cf37..0d796d064edbc17ce194b35e6e8bc2236e607d69 100644 (file)
@@ -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',
@@ -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,
@@ -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'