From 94f5a63fdc86f17f1dd17ed16534fbdecb8c448f Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 30 Sep 2024 16:10:19 +0200 Subject: [PATCH] fix(types): allow writable getters Fix #2767 --- packages/pinia/__tests__/getters.spec.ts | 43 ++++++++++++++++++++++++ packages/pinia/src/store.ts | 2 ++ packages/pinia/src/types.ts | 28 ++++++++++++--- packages/pinia/test-dts/store.test-d.ts | 34 +++++++++++++++++-- 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/packages/pinia/__tests__/getters.spec.ts b/packages/pinia/__tests__/getters.spec.ts index b0c1d96826..2b346031d8 100644 --- a/packages/pinia/__tests__/getters.spec.ts +++ b/packages/pinia/__tests__/getters.spec.ts @@ -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() diff --git a/packages/pinia/src/store.ts b/packages/pinia/src/store.ts index 923c4b120b..3bd239bdfb 100644 --- a/packages/pinia/src/store.ts +++ b/packages/pinia/src/store.ts @@ -42,6 +42,8 @@ import { DefineStoreOptionsInPlugin, StoreGeneric, _StoreWithGetters, + _StoreWithGetters_Readonly, + _StoreWithGetters_Writable, _ExtractActionsFromSetupStore, _ExtractGettersFromSetupStore, _ExtractStateFromSetupStore, diff --git a/packages/pinia/src/types.ts b/packages/pinia/src/types.ts index f36288a888..3a3f5da96c 100644 --- a/packages/pinia/src/types.ts +++ b/packages/pinia/src/types.ts @@ -4,6 +4,7 @@ import type { Ref, UnwrapRef, WatchOptions, + WritableComputedRef, } from 'vue-demi' import { Pinia } from './rootStore' @@ -451,10 +452,29 @@ export type _StoreWithActions = { * Store augmented with getters. For internal usage only. * For internal use **only** */ -export type _StoreWithGetters = { - readonly [k in keyof G]: G[k] extends (...args: any[]) => infer R - ? R - : UnwrapRef +export type _StoreWithGetters = _StoreWithGetters_Readonly & + _StoreWithGetters_Writable + +/** + * Store augmented with readonly getters. For internal usage **only**. + */ +export type _StoreWithGetters_Readonly = { + 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 +} + +/** + * Store augmented with writable getters. For internal usage **only**. + */ +export type _StoreWithGetters_Writable = { + [K in keyof G as G[K] extends WritableComputedRef + ? 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 ? R : never } /** diff --git a/packages/pinia/test-dts/store.test-d.ts b/packages/pinia/test-dts/store.test-d.ts index 2193f9f8b6..0d796d064e 100644 --- a/packages/pinia/test-dts/store.test-d.ts +++ b/packages/pinia/test-dts/store.test-d.ts @@ -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(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({ + 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(writableComputedStore.bananasAmount) +// should allow writing to it +writableComputedStore.bananasAmount = 0 +expectType(writableComputedStore.bananas) +// should allow setting a different type +// @ts-expect-error: still not doable +writableComputedStore.bananas = 'hello'