diff --git a/modules/signals/spec/get-state.spec.ts b/modules/signals/spec/get-state.spec.ts deleted file mode 100644 index 92af67fbcb..0000000000 --- a/modules/signals/spec/get-state.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { effect } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { getState, patchState, signalStore, withState } from '../src'; - -describe('getState', () => { - const initialState = { - user: { - firstName: 'John', - lastName: 'Smith', - }, - foo: 'bar', - numbers: [1, 2, 3], - ngrx: 'signals', - }; - - describe('with signalStore', () => { - it('returns the state object', () => { - const Store = signalStore(withState(initialState)); - const store = new Store(); - - expect(getState(store)).toEqual(initialState); - - patchState(store, { foo: 'baz' }); - - expect(getState(store)).toEqual({ ...initialState, foo: 'baz' }); - }); - - it('executes in the reactive context', () => { - const Store = signalStore(withState(initialState)); - const store = new Store(); - - let executionCount = 0; - - TestBed.runInInjectionContext(() => { - effect(() => { - getState(store); - executionCount++; - }); - }); - - TestBed.flushEffects(); - expect(executionCount).toBe(1); - - patchState(store, { foo: 'baz' }); - - TestBed.flushEffects(); - expect(executionCount).toBe(2); - }); - }); -}); diff --git a/modules/signals/spec/patch-state.spec.ts b/modules/signals/spec/patch-state.spec.ts deleted file mode 100644 index 66aab99e66..0000000000 --- a/modules/signals/spec/patch-state.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { patchState, signalState, signalStore, withState } from '../src'; -import { STATE_SIGNAL } from '../src/state-signal'; - -describe('patchState', () => { - const initialState = { - user: { - firstName: 'John', - lastName: 'Smith', - }, - foo: 'bar', - numbers: [1, 2, 3], - ngrx: 'signals', - }; - - [ - { - name: 'with signalState', - stateFactory: () => signalState(initialState), - }, - { - name: 'with signalStore', - stateFactory: () => { - const SignalStore = signalStore(withState(initialState)); - return new SignalStore(); - }, - }, - ].forEach(({ name, stateFactory }) => { - describe(name, () => { - it('patches state via partial state object', () => { - const state = stateFactory(); - - patchState(state, { - user: { firstName: 'Johannes', lastName: 'Schmidt' }, - foo: 'baz', - }); - - expect(state[STATE_SIGNAL]()).toEqual({ - ...initialState, - user: { firstName: 'Johannes', lastName: 'Schmidt' }, - foo: 'baz', - }); - }); - - it('patches state via updater function', () => { - const state = stateFactory(); - - patchState(state, (state) => ({ - numbers: [...state.numbers, 4], - ngrx: 'rocks', - })); - - expect(state[STATE_SIGNAL]()).toEqual({ - ...initialState, - numbers: [1, 2, 3, 4], - ngrx: 'rocks', - }); - }); - - it('patches state via sequence of partial state objects and updater functions', () => { - const state = stateFactory(); - - patchState( - state, - { user: { firstName: 'Johannes', lastName: 'Schmidt' } }, - (state) => ({ numbers: [...state.numbers, 4], foo: 'baz' }), - (state) => ({ user: { ...state.user, firstName: 'Jovan' } }), - { foo: 'foo' } - ); - - expect(state[STATE_SIGNAL]()).toEqual({ - ...initialState, - user: { firstName: 'Jovan', lastName: 'Schmidt' }, - foo: 'foo', - numbers: [1, 2, 3, 4], - }); - }); - - it('patches state immutably', () => { - const state = stateFactory(); - - patchState(state, { - foo: 'bar', - numbers: [3, 2, 1], - ngrx: 'rocks', - }); - - expect(state.user()).toBe(initialState.user); - expect(state.foo()).toBe(initialState.foo); - expect(state.numbers()).not.toBe(initialState.numbers); - expect(state.ngrx()).not.toBe(initialState.ngrx); - }); - }); - }); -}); diff --git a/modules/signals/spec/signal-state.spec.ts b/modules/signals/spec/signal-state.spec.ts index 18e0a66ec4..a6ffe60363 100644 --- a/modules/signals/spec/signal-state.spec.ts +++ b/modules/signals/spec/signal-state.spec.ts @@ -2,7 +2,7 @@ import * as angular from '@angular/core'; import { effect, isSignal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { patchState, signalState } from '../src'; -import { STATE_SIGNAL } from '../src/state-signal'; +import { STATE_SOURCE } from '../src/state-source'; describe('signalState', () => { const initialState = { @@ -15,12 +15,12 @@ describe('signalState', () => { ngrx: 'signals', }; - it('has state signal', () => { + it('has state source', () => { const state = signalState({}); - const stateSignal = state[STATE_SIGNAL]; + const stateSource = state[STATE_SOURCE]; - expect(isSignal(stateSignal)).toBe(true); - expect(typeof stateSignal.update === 'function').toBe(true); + expect(isSignal(stateSource)).toBe(true); + expect(typeof stateSource.update === 'function').toBe(true); }); it('creates signals for nested state slices', () => { @@ -76,13 +76,13 @@ describe('signalState', () => { expect((state.user.firstName as any).y).toBe(undefined); }); - it('does not modify STATE_SIGNAL', () => { + it('does not modify STATE_SOURCE', () => { const state = signalState(initialState); - expect((state[STATE_SIGNAL] as any).user).toBe(undefined); - expect((state[STATE_SIGNAL] as any).foo).toBe(undefined); - expect((state[STATE_SIGNAL] as any).numbers).toBe(undefined); - expect((state[STATE_SIGNAL] as any).ngrx).toBe(undefined); + expect((state[STATE_SOURCE] as any).user).toBe(undefined); + expect((state[STATE_SOURCE] as any).foo).toBe(undefined); + expect((state[STATE_SOURCE] as any).numbers).toBe(undefined); + expect((state[STATE_SOURCE] as any).ngrx).toBe(undefined); }); it('overrides Function properties if state keys have the same name', () => { diff --git a/modules/signals/spec/signal-store-feature.spec.ts b/modules/signals/spec/signal-store-feature.spec.ts index f3dfd5998b..2ad0cbf23f 100644 --- a/modules/signals/spec/signal-store-feature.spec.ts +++ b/modules/signals/spec/signal-store-feature.spec.ts @@ -7,7 +7,7 @@ import { withMethods, withState, } from '../src'; -import { STATE_SIGNAL } from '../src/state-signal'; +import { STATE_SOURCE } from '../src/state-source'; describe('signalStoreFeature', () => { function withCustomFeature1() { @@ -24,7 +24,6 @@ describe('signalStoreFeature', () => { return signalStoreFeature( withCustomFeature1(), withMethods(({ foo, baz }) => ({ - bar: (value: number) => value, m: () => foo() + baz() + 3, })) ); @@ -51,7 +50,7 @@ describe('signalStoreFeature', () => { const store = new Store(); - expect(store[STATE_SIGNAL]()).toEqual({ foo: 'foo' }); + expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('foo1'); expect(store.baz()).toBe('foofoo12'); @@ -66,9 +65,9 @@ describe('signalStoreFeature', () => { const store = new Store(); - expect(store[STATE_SIGNAL]()).toEqual({ foo: 'foo' }); + expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); expect(store.foo()).toBe('foo'); - expect(store.bar(10)).toBe(10); + expect(store.bar()).toBe('foo1'); expect(store.m()).toBe('foofoofoo123'); expect(store.m1()).toBe('foo10'); }); @@ -82,7 +81,7 @@ describe('signalStoreFeature', () => { const store = new Store(); - expect(store[STATE_SIGNAL]()).toEqual({ foo: 'foo', foo1: 1, foo2: 2 }); + expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo', foo1: 1, foo2: 2 }); expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('foo1'); expect(store.baz()).toBe('foofoo12'); diff --git a/modules/signals/spec/signal-store.spec.ts b/modules/signals/spec/signal-store.spec.ts index 036d226b8c..afe77782d6 100644 --- a/modules/signals/spec/signal-store.spec.ts +++ b/modules/signals/spec/signal-store.spec.ts @@ -8,7 +8,7 @@ import { withMethods, withState, } from '../src'; -import { STATE_SIGNAL } from '../src/state-signal'; +import { STATE_SOURCE } from '../src/state-source'; import { createLocalService } from './helpers'; describe('signalStore', () => { @@ -17,11 +17,11 @@ describe('signalStore', () => { const Store = signalStore(withState({})); const store = new Store(); - const stateSignal = store[STATE_SIGNAL]; + const stateSource = store[STATE_SOURCE]; - expect(isSignal(stateSignal)).toBe(true); - expect(typeof stateSignal.update === 'function').toBe(true); - expect(stateSignal()).toEqual({}); + expect(isSignal(stateSource)).toBe(true); + expect(typeof stateSource.update === 'function').toBe(true); + expect(stateSource()).toEqual({}); }); it('creates a store as injectable service', () => { @@ -29,11 +29,11 @@ describe('signalStore', () => { TestBed.configureTestingModule({ providers: [Store] }); const store = TestBed.inject(Store); - const stateSignal = store[STATE_SIGNAL]; + const stateSource = store[STATE_SOURCE]; - expect(isSignal(stateSignal)).toBe(true); - expect(typeof stateSignal.update === 'function').toBe(true); - expect(stateSignal()).toEqual({}); + expect(isSignal(stateSource)).toBe(true); + expect(typeof stateSource.update === 'function').toBe(true); + expect(stateSource()).toEqual({}); }); it('creates a store that is provided in root when providedIn option is specified', () => { @@ -41,12 +41,12 @@ describe('signalStore', () => { const store1 = TestBed.inject(Store); const store2 = TestBed.inject(Store); - const stateSignal = store1[STATE_SIGNAL]; + const stateSource = store1[STATE_SOURCE]; expect(store1).toBe(store2); - expect(isSignal(stateSignal)).toBe(true); - expect(typeof stateSignal.update === 'function').toBe(true); - expect(stateSignal()).toEqual({}); + expect(isSignal(stateSource)).toBe(true); + expect(typeof stateSource.update === 'function').toBe(true); + expect(stateSource()).toEqual({}); }); }); @@ -61,7 +61,7 @@ describe('signalStore', () => { const store = new Store(); - expect(store[STATE_SIGNAL]()).toEqual({ + expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo', x: { y: { z: 10 } }, }); @@ -129,7 +129,7 @@ describe('signalStore', () => { const store = new Store(); - expect(store[STATE_SIGNAL]()).toEqual({ foo: 'foo' }); + expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('bar'); expect(store.baz()).toBe('baz'); @@ -156,7 +156,7 @@ describe('signalStore', () => { withComputed(() => ({ bar: signal('bar').asReadonly() })), withMethods(() => ({ baz: () => 'baz' })), withMethods((store) => { - expect(store[STATE_SIGNAL]()).toEqual({ foo: 'foo' }); + expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('bar'); expect(store.baz()).toBe('baz'); @@ -167,7 +167,7 @@ describe('signalStore', () => { const store = new Store(); - expect(store[STATE_SIGNAL]()).toEqual({ foo: 'foo' }); + expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('bar'); expect(store.baz()).toBe('baz'); @@ -233,7 +233,7 @@ describe('signalStore', () => { withMethods(() => ({ baz: () => 'baz' })), withHooks({ onInit(store) { - expect(store[STATE_SIGNAL]()).toEqual({ foo: 'foo' }); + expect(store[STATE_SOURCE]()).toEqual({ foo: 'foo' }); expect(store.foo()).toBe('foo'); expect(store.bar()).toBe('bar'); expect(store.baz()).toBe('baz'); @@ -340,43 +340,39 @@ describe('signalStore', () => { }); describe('composition', () => { - it('overrides previously defined store properties immutably', () => { + it('logs warning if previously defined signal store members have the same name', () => { const Store = signalStore( withState({ i: 1, j: 2, k: 3, l: 4 }), - withComputed(({ i, j, k, l }) => { - expect(i()).toBe(1); - expect(j()).toBe(2); - expect(k()).toBe(3); - expect(l()).toBe(4); - - return { - l: signal('l').asReadonly(), - m: signal('m').asReadonly(), - }; - }), - withMethods((store) => { - expect(store.i()).toBe(1); - expect(store.j()).toBe(2); - expect(store.k()).toBe(3); - expect(store.l()).toBe('l'); - expect(store.m()).toBe('m'); - - return { - j: () => 'j', - m: () => true, - n: (value: number) => value, - }; - }) + withComputed(() => ({ + l: signal('l').asReadonly(), + m: signal('m').asReadonly(), + })), + withMethods(() => ({ + j: () => 'j', + m: () => true, + n: (value: number) => value, + })) ); + const warnings: string[][] = []; + jest + .spyOn(console, 'warn') + .mockImplementation((...args: string[]) => warnings.push(args)); - const store = new Store(); + new Store(); - expect(store.i()).toBe(1); - expect(store.j()).toBe('j'); - expect(store.k()).toBe(3); - expect(store.l()).toBe('l'); - expect(store.m()).toBe(true); - expect(store.n(10)).toBe(10); + expect(console.warn).toHaveBeenCalledTimes(2); + expect(warnings).toEqual([ + [ + '@ngrx/signals: SignalStore members cannot be overridden.', + 'Trying to override:', + 'l', + ], + [ + '@ngrx/signals: SignalStore members cannot be overridden.', + 'Trying to override:', + 'j, m', + ], + ]); }); }); }); diff --git a/modules/signals/spec/state-source.spec.ts b/modules/signals/spec/state-source.spec.ts new file mode 100644 index 0000000000..e5db83529b --- /dev/null +++ b/modules/signals/spec/state-source.spec.ts @@ -0,0 +1,141 @@ +import { effect } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + getState, + patchState, + signalState, + signalStore, + withState, +} from '../src'; +import { STATE_SOURCE } from '../src/state-source'; + +describe('StateSource', () => { + const initialState = { + user: { + firstName: 'John', + lastName: 'Smith', + }, + foo: 'bar', + numbers: [1, 2, 3], + ngrx: 'signals', + }; + + describe('patchState', () => { + [ + { + name: 'with signalState', + stateFactory: () => signalState(initialState), + }, + { + name: 'with signalStore', + stateFactory: () => { + const SignalStore = signalStore(withState(initialState)); + return new SignalStore(); + }, + }, + ].forEach(({ name, stateFactory }) => { + describe(name, () => { + it('patches state via partial state object', () => { + const state = stateFactory(); + + patchState(state, { + user: { firstName: 'Johannes', lastName: 'Schmidt' }, + foo: 'baz', + }); + + expect(state[STATE_SOURCE]()).toEqual({ + ...initialState, + user: { firstName: 'Johannes', lastName: 'Schmidt' }, + foo: 'baz', + }); + }); + + it('patches state via updater function', () => { + const state = stateFactory(); + + patchState(state, (state) => ({ + numbers: [...state.numbers, 4], + ngrx: 'rocks', + })); + + expect(state[STATE_SOURCE]()).toEqual({ + ...initialState, + numbers: [1, 2, 3, 4], + ngrx: 'rocks', + }); + }); + + it('patches state via sequence of partial state objects and updater functions', () => { + const state = stateFactory(); + + patchState( + state, + { user: { firstName: 'Johannes', lastName: 'Schmidt' } }, + (state) => ({ numbers: [...state.numbers, 4], foo: 'baz' }), + (state) => ({ user: { ...state.user, firstName: 'Jovan' } }), + { foo: 'foo' } + ); + + expect(state[STATE_SOURCE]()).toEqual({ + ...initialState, + user: { firstName: 'Jovan', lastName: 'Schmidt' }, + foo: 'foo', + numbers: [1, 2, 3, 4], + }); + }); + + it('patches state immutably', () => { + const state = stateFactory(); + + patchState(state, { + foo: 'bar', + numbers: [3, 2, 1], + ngrx: 'rocks', + }); + + expect(state.user()).toBe(initialState.user); + expect(state.foo()).toBe(initialState.foo); + expect(state.numbers()).not.toBe(initialState.numbers); + expect(state.ngrx()).not.toBe(initialState.ngrx); + }); + }); + }); + }); + + describe('getState', () => { + describe('with signalStore', () => { + it('returns the state object', () => { + const Store = signalStore(withState(initialState)); + const store = new Store(); + + expect(getState(store)).toEqual(initialState); + + patchState(store, { foo: 'baz' }); + + expect(getState(store)).toEqual({ ...initialState, foo: 'baz' }); + }); + + it('executes in the reactive context', () => { + const Store = signalStore(withState(initialState)); + const store = new Store(); + + let executionCount = 0; + + TestBed.runInInjectionContext(() => { + effect(() => { + getState(store); + executionCount++; + }); + }); + + TestBed.flushEffects(); + expect(executionCount).toBe(1); + + patchState(store, { foo: 'baz' }); + + TestBed.flushEffects(); + expect(executionCount).toBe(2); + }); + }); + }); +}); diff --git a/modules/signals/spec/types/patch-state.types.spec.ts b/modules/signals/spec/types/patch-state.types.spec.ts index c52763d5cf..8542826c74 100644 --- a/modules/signals/spec/types/patch-state.types.spec.ts +++ b/modules/signals/spec/types/patch-state.types.spec.ts @@ -27,7 +27,7 @@ describe('patchState', () => { compilerOptions() ); - it('infers the state type from StateSignal', () => { + it('infers the state type from StateSource', () => { expectSnippet('patchState(state, increment())').toSucceed(); expectSnippet("patchState(state, { foo: 'baz' })").toSucceed(); expectSnippet("patchState(state, { foo: 'baz' }, increment())").toSucceed(); diff --git a/modules/signals/spec/types/signal-store.types.spec.ts b/modules/signals/spec/types/signal-store.types.spec.ts index 9a37586686..d973b4338d 100644 --- a/modules/signals/spec/types/signal-store.types.spec.ts +++ b/modules/signals/spec/types/signal-store.types.spec.ts @@ -33,7 +33,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'Store', - 'Type<{ foo: Signal; bar: Signal; } & StateSignal<{ foo: string; bar: number[]; }>>' + 'Type<{ foo: Signal; bar: Signal; } & StateSource<{ foo: string; bar: number[]; }>>' ); }); @@ -63,7 +63,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ user: DeepSignal<{ age: number; details: { first: string; flags: boolean[]; }; }>; } & StateSignal<{ user: { age: number; details: { first: string; flags: boolean[]; }; }; }>' + '{ user: DeepSignal<{ age: number; details: { first: string; flags: boolean[]; }; }>; } & StateSource<{ user: { age: number; details: { first: string; flags: boolean[]; }; }; }>' ); expectSnippet(snippet).toInfer( @@ -203,7 +203,7 @@ describe('signalStore', () => { expectSnippet(snippet).toSucceed(); - expectSnippet(snippet).toInfer('Store', 'Type<{} & StateSignal<{}>>'); + expectSnippet(snippet).toInfer('Store', 'Type<{} & StateSource<{}>>'); }); it('succeeds when state slices are union types', () => { @@ -234,7 +234,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ foo: Signal; bar: DeepSignal<{ baz: { b: boolean; } | null; }>; x: DeepSignal<{ y: { z: number | undefined; }; }>; } & StateSignal<{ foo: number | { ...; }; bar: { ...; }; x: { ...; }; }>' + '{ foo: Signal; bar: DeepSignal<{ baz: { b: boolean; } | null; }>; x: DeepSignal<{ y: { z: number | undefined; }; }>; } & StateSource<{ foo: number | { ...; }; bar: { ...; }; x: { ...; }; }>' ); expectSnippet(snippet).toInfer('foo', 'Signal'); @@ -274,7 +274,7 @@ describe('signalStore', () => { expectSnippet(snippet1).toInfer( 'Store', - 'Type<{ name: DeepSignal<{ x: { y: string; }; }>; arguments: Signal; call: Signal; } & StateSignal<{ name: { x: { y: string; }; }; arguments: number[]; call: boolean; }>>' + 'Type<{ name: DeepSignal<{ x: { y: string; }; }>; arguments: Signal; call: Signal; } & StateSource<{ name: { x: { y: string; }; }; arguments: number[]; call: boolean; }>>' ); const snippet2 = ` @@ -291,7 +291,7 @@ describe('signalStore', () => { expectSnippet(snippet2).toInfer( 'Store', - 'Type<{ apply: Signal; bind: DeepSignal<{ foo: string; }>; prototype: Signal; } & StateSignal<{ apply: string; bind: { foo: string; }; prototype: string[]; }>>' + 'Type<{ apply: Signal; bind: DeepSignal<{ foo: string; }>; prototype: Signal; } & StateSource<{ apply: string; bind: { foo: string; }; prototype: string[]; }>>' ); const snippet3 = ` @@ -307,7 +307,7 @@ describe('signalStore', () => { expectSnippet(snippet3).toInfer( 'Store', - 'Type<{ length: Signal; caller: Signal; } & StateSignal<{ length: number; caller: undefined; }>>' + 'Type<{ length: Signal; caller: Signal; } & StateSource<{ length: number; caller: undefined; }>>' ); }); @@ -362,7 +362,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ bar: DeepSignal<{ baz?: number | undefined; }>; x: DeepSignal<{ y?: { z: boolean; } | undefined; }>; } & StateSignal<{ bar: { baz?: number | undefined; }; x: { y?: { z: boolean; } | undefined; }; }>' + '{ bar: DeepSignal<{ baz?: number | undefined; }>; x: DeepSignal<{ y?: { z: boolean; } | undefined; }>; } & StateSource<{ bar: { baz?: number | undefined; }; x: { y?: { z: boolean; } | undefined; }; }>' ); expectSnippet(snippet).toInfer( @@ -591,7 +591,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ ngrx: Signal; x: DeepSignal<{ y: string; }>; signals: Signal; mgmt: (arg: boolean) => number; } & StateSignal<{ ngrx: string; x: { y: string; }; }>' + '{ ngrx: Signal; x: DeepSignal<{ y: string; }>; signals: Signal; mgmt: (arg: boolean) => number; } & StateSource<{ ngrx: string; x: { y: string; }; }>' ); }); @@ -619,7 +619,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ foo: Signal; bar: Signal; baz: (x: number) => void; } & StateSignal<{ foo: number; }>' + '{ foo: Signal; bar: Signal; baz: (x: number) => void; } & StateSource<{ foo: number; }>' ); }); @@ -723,20 +723,20 @@ describe('signalStore', () => { ${baseSnippet} const Store = signalStore( - withComputed(() => ({ sig: computed(() => 1) })), - withMethods(() => ({ q1: () => false })), + withMethods((store) => ({ + f() {}, + g() {}, + })), withComputed(() => ({ sig: computed(() => false) })), withState({ q1: 'q1', q2: 'q2' }), withX(), withY(), - withComputed(() => ({ q1: computed(() => 10) })), - withMethods((store) => ({ - f() { - patchState(store, { x: 1, y: { a: '', b: 0 }, q2: 'q2new' }); - }, - })), withZ() ); + `).toSucceed(); + + expectSnippet(` + ${baseSnippet} const feature = signalStoreFeature( { computed: type<{ sig: Signal }>() }, @@ -766,9 +766,7 @@ describe('signalStore', () => { ${baseSnippet} const Store = signalStore( - withComputed(() => ({ sig: computed(() => 1) })), - withState({ q1: 'q1', q2: 'q2' }), - withState({ q1: 1 }), + withState({ q1: 1, q2: 'q2' }), withComputed(() => ({ sig: computed(() => false) })), withX(), withY(), @@ -785,8 +783,7 @@ describe('signalStore', () => { ${baseSnippet} const feature = signalStoreFeature( - { computed: type<{ sig: Signal }>() }, - withComputed(() => ({ sig: computed(() => 1) })), + { computed: type<{ sig: Signal }>() }, withX(), withState({ q1: 'q1' }), withY(), @@ -802,32 +799,28 @@ describe('signalStore', () => { ${baseSnippet} const Store = signalStore( - withComputed(() => ({ sig: computed(() => 1) })), - withState({ q1: 1 }), withState({ q1: 'q1', q2: 'q2' }), withComputed(() => ({ sig: computed(() => false) })), withX(), - withY(), - withComputed(() => ({ q1: computed(() => 10) })), withMethods((store) => ({ f() { - patchState(store, { x: 1, y: { a: '', b: 0 }, q2: 'q2new' }); + patchState(store, { q1: 'q1new', q2: 'q2new', x: 100 }); }, - })) + g: (str: string) => console.log(str), + })), + withY(), + withZ(), ); const feature = signalStoreFeature( - { computed: type<{ sig: Signal }>() }, - withComputed(() => ({ sig: computed(() => 1) })), + { + computed: type<{ sig: Signal }>(), + methods: type<{ f(): void; g(arg: string): string; }>(), + }, withX(), + withZ(), withState({ q1: 'q1' }), - withComputed(() => ({ sig: computed(() => false) })), withY(), - withMethods((store) => ({ - f() { - patchState(store, { x: 1, q1: 'xyz', y: { a: '', b: 0 } }); - }, - })) ); `).toSucceed(); }); diff --git a/modules/signals/spec/with-computed.spec.ts b/modules/signals/spec/with-computed.spec.ts index 106516d396..cf6be19b1b 100644 --- a/modules/signals/spec/with-computed.spec.ts +++ b/modules/signals/spec/with-computed.spec.ts @@ -18,7 +18,7 @@ describe('withComputed', () => { expect(store.computedSignals.s2).toBe(s2); }); - it('overrides previously defined state signals, computed signals, and methods with the same name', () => { + it('logs warning if previously defined signal store members have the same name', () => { const initialStore = [ withState({ p1: 10, @@ -33,25 +33,22 @@ describe('withComputed', () => { m2() {}, })), ].reduce((acc, feature) => feature(acc), getInitialInnerStore()); - const s2 = signal(10).asReadonly(); - const store = withComputed(() => ({ + jest.spyOn(console, 'warn').mockImplementation(); + + withComputed(() => ({ + p: signal(0).asReadonly(), p1: signal('p1').asReadonly(), s2, m1: signal({ m: 1 }).asReadonly(), + m3: signal({ m: 3 }).asReadonly(), s3: signal({ s: 3 }).asReadonly(), }))(initialStore); - expect(Object.keys(store.computedSignals)).toEqual([ - 's1', - 's2', - 'p1', - 'm1', - 's3', - ]); - expect(store.computedSignals.s2).toBe(s2); - - expect(Object.keys(store.stateSignals)).toEqual(['p2']); - expect(Object.keys(store.methods)).toEqual(['m2']); + expect(console.warn).toHaveBeenCalledWith( + '@ngrx/signals: SignalStore members cannot be overridden.', + 'Trying to override:', + 'p1, s2, m1' + ); }); }); diff --git a/modules/signals/spec/with-methods.spec.ts b/modules/signals/spec/with-methods.spec.ts index 3e6e59309f..b2ae30410b 100644 --- a/modules/signals/spec/with-methods.spec.ts +++ b/modules/signals/spec/with-methods.spec.ts @@ -18,7 +18,7 @@ describe('withMethods', () => { expect(store.methods.m2).toBe(m2); }); - it('overrides previously defined state signals, computed signals, and methods with the same name', () => { + it('logs warning if previously defined signal store members have the same name', () => { const initialStore = [ withState({ p1: 'p1', @@ -33,19 +33,22 @@ describe('withMethods', () => { m2() {}, })), ].reduce((acc, feature) => feature(acc), getInitialInnerStore()); - const m2 = () => 10; - const store = withMethods(() => ({ + jest.spyOn(console, 'warn').mockImplementation(); + + withMethods(() => ({ + p() {}, p2() {}, + s: () => {}, s1: () => 100, m2, m3: () => 'm3', }))(initialStore); - expect(Object.keys(store.methods)).toEqual(['m1', 'm2', 'p2', 's1', 'm3']); - expect(store.methods.m2).toBe(m2); - - expect(Object.keys(store.stateSignals)).toEqual(['p1']); - expect(Object.keys(store.computedSignals)).toEqual(['s2']); + expect(console.warn).toHaveBeenCalledWith( + '@ngrx/signals: SignalStore members cannot be overridden.', + 'Trying to override:', + 'p2, s1, m2' + ); }); }); diff --git a/modules/signals/spec/with-state.spec.ts b/modules/signals/spec/with-state.spec.ts index bccc702ea9..6ea42db0cb 100644 --- a/modules/signals/spec/with-state.spec.ts +++ b/modules/signals/spec/with-state.spec.ts @@ -1,18 +1,18 @@ import { isSignal, signal } from '@angular/core'; import { withComputed, withMethods, withState } from '../src'; -import { STATE_SIGNAL } from '../src/state-signal'; +import { STATE_SOURCE } from '../src/state-source'; import { getInitialInnerStore } from '../src/signal-store'; describe('withState', () => { - it('patches state signal and updates slices immutably', () => { + it('patches state source and updates slices immutably', () => { const initialStore = getInitialInnerStore(); - const initialState = initialStore[STATE_SIGNAL](); + const initialState = initialStore[STATE_SOURCE](); const store = withState({ foo: 'bar', x: { y: 'z' }, })(initialStore); - const state = store[STATE_SIGNAL](); + const state = store[STATE_SOURCE](); expect(state).toEqual({ foo: 'bar', x: { y: 'z' } }); expect(initialState).toEqual({}); @@ -39,14 +39,14 @@ describe('withState', () => { expect(isSignal(store.stateSignals.x.y)).toBe(true); }); - it('patches state signal and creates deep signals for state slices provided via factory', () => { + it('patches state source and creates deep signals for state slices provided via factory', () => { const initialStore = getInitialInnerStore(); const store = withState(() => ({ foo: 'bar', x: { y: 'z' }, }))(initialStore); - const state = store[STATE_SIGNAL](); + const state = store[STATE_SOURCE](); expect(state).toEqual({ foo: 'bar', x: { y: 'z' } }); expect(store.stateSignals.foo()).toBe('bar'); @@ -54,7 +54,7 @@ describe('withState', () => { expect(store.stateSignals.x.y()).toBe('z'); }); - it('overrides previously defined state signals, computed signals, and methods with the same name', () => { + it('logs warning if previously defined signal store members have the same name', () => { const initialStore = [ withState({ p1: 10, @@ -69,24 +69,21 @@ describe('withState', () => { m2() {}, })), ].reduce((acc, feature) => feature(acc), getInitialInnerStore()); + jest.spyOn(console, 'warn').mockImplementation(); - const store = withState(() => ({ + withState(() => ({ p2: 100, + s: 's', s2: 's2', + m: { s: 10 }, m2: { m: 2 }, p3: 'p3', }))(initialStore); - expect(Object.keys(store.stateSignals)).toEqual([ - 'p1', - 'p2', - 's2', - 'm2', - 'p3', - ]); - expect(store.stateSignals.p2()).toBe(100); - - expect(Object.keys(store.computedSignals)).toEqual(['s1']); - expect(Object.keys(store.methods)).toEqual(['m1']); + expect(console.warn).toHaveBeenCalledWith( + '@ngrx/signals: SignalStore members cannot be overridden.', + 'Trying to override:', + 'p2, s2, m2' + ); }); }); diff --git a/modules/signals/src/get-state.ts b/modules/signals/src/get-state.ts deleted file mode 100644 index 0d3c2b5d4a..0000000000 --- a/modules/signals/src/get-state.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { STATE_SIGNAL, StateSignal } from './state-signal'; - -export function getState( - stateSignal: StateSignal -): State { - return stateSignal[STATE_SIGNAL](); -} diff --git a/modules/signals/src/helpers.ts b/modules/signals/src/helpers.ts deleted file mode 100644 index e7f731aaa6..0000000000 --- a/modules/signals/src/helpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function excludeKeys< - Obj extends Record, - Keys extends string[] ->(obj: Obj, keys: Keys): Omit { - return Object.keys(obj).reduce>((acc, key) => { - if (!keys.includes(key)) { - acc[key] = obj[key]; - } - return acc; - }, {}) as Omit; -} diff --git a/modules/signals/src/index.ts b/modules/signals/src/index.ts index e115ca9b1a..e3563985d2 100644 --- a/modules/signals/src/index.ts +++ b/modules/signals/src/index.ts @@ -1,11 +1,14 @@ export { DeepSignal } from './deep-signal'; -export { getState } from './get-state'; -export { PartialStateUpdater, patchState } from './patch-state'; -export { signalState } from './signal-state'; +export { signalState, SignalState } from './signal-state'; export { signalStore } from './signal-store'; export { signalStoreFeature, type } from './signal-store-feature'; export { SignalStoreFeature } from './signal-store-models'; -export { StateSignal } from './state-signal'; +export { + getState, + PartialStateUpdater, + patchState, + StateSource, +} from './state-source'; export { withComputed } from './with-computed'; export { withHooks } from './with-hooks'; diff --git a/modules/signals/src/signal-state.ts b/modules/signals/src/signal-state.ts index 2083c901fb..e0c7fd8bb6 100644 --- a/modules/signals/src/signal-state.ts +++ b/modules/signals/src/signal-state.ts @@ -1,17 +1,18 @@ import { signal } from '@angular/core'; -import { STATE_SIGNAL, StateSignal } from './state-signal'; +import { STATE_SOURCE, StateSource } from './state-source'; import { DeepSignal, toDeepSignal } from './deep-signal'; -type SignalState = DeepSignal & StateSignal; +export type SignalState = DeepSignal & + StateSource; export function signalState( initialState: State ): SignalState { - const stateSignal = signal(initialState as State); - const deepSignal = toDeepSignal(stateSignal.asReadonly()); - Object.defineProperty(deepSignal, STATE_SIGNAL, { - value: stateSignal, + const stateSource = signal(initialState as State); + const signalState = toDeepSignal(stateSource.asReadonly()); + Object.defineProperty(signalState, STATE_SOURCE, { + value: stateSource, }); - return deepSignal as SignalState; + return signalState as SignalState; } diff --git a/modules/signals/src/signal-store-assertions.ts b/modules/signals/src/signal-store-assertions.ts new file mode 100644 index 0000000000..be0a1947c5 --- /dev/null +++ b/modules/signals/src/signal-store-assertions.ts @@ -0,0 +1,29 @@ +import { InnerSignalStore } from './signal-store-models'; + +declare const ngDevMode: unknown; + +export function assertUniqueStoreMembers( + store: InnerSignalStore, + newMemberKeys: string[] +): void { + if (!ngDevMode) { + return; + } + + const storeMembers = { + ...store.stateSignals, + ...store.computedSignals, + ...store.methods, + }; + const overriddenKeys = Object.keys(storeMembers).filter((memberKey) => + newMemberKeys.includes(memberKey) + ); + + if (overriddenKeys.length > 0) { + console.warn( + '@ngrx/signals: SignalStore members cannot be overridden.', + 'Trying to override:', + overriddenKeys.join(', ') + ); + } +} diff --git a/modules/signals/src/signal-store-feature.ts b/modules/signals/src/signal-store-feature.ts index 9ef2c86c55..f9c10ec756 100644 --- a/modules/signals/src/signal-store-feature.ts +++ b/modules/signals/src/signal-store-feature.ts @@ -1,11 +1,16 @@ import { EmptyFeatureResult, - MergeFeatureResults, SignalStoreFeature, SignalStoreFeatureResult, } from './signal-store-models'; import { Prettify } from './ts-helpers'; +type PrettifyFeatureResult = Prettify<{ + state: Prettify; + computed: Prettify; + methods: Prettify; +}>; + export function signalStoreFeature( f1: SignalStoreFeature ): SignalStoreFeature; @@ -15,7 +20,7 @@ export function signalStoreFeature< >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2> -): SignalStoreFeature>; +): SignalStoreFeature>; export function signalStoreFeature< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -23,8 +28,8 @@ export function signalStoreFeature< >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3> -): SignalStoreFeature>; + f3: SignalStoreFeature +): SignalStoreFeature>; export function signalStoreFeature< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -33,11 +38,11 @@ export function signalStoreFeature< >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4> + f3: SignalStoreFeature, + f4: SignalStoreFeature ): SignalStoreFeature< EmptyFeatureResult, - MergeFeatureResults<[F1, F2, F3, F4]> + PrettifyFeatureResult >; export function signalStoreFeature< F1 extends SignalStoreFeatureResult, @@ -48,12 +53,12 @@ export function signalStoreFeature< >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5> + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature ): SignalStoreFeature< EmptyFeatureResult, - MergeFeatureResults<[F1, F2, F3, F4, F5]> + PrettifyFeatureResult >; export function signalStoreFeature< F1 extends SignalStoreFeatureResult, @@ -65,13 +70,13 @@ export function signalStoreFeature< >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5>, - f6: SignalStoreFeature, F6> + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature, + f6: SignalStoreFeature ): SignalStoreFeature< EmptyFeatureResult, - MergeFeatureResults<[F1, F2, F3, F4, F5, F6]> + PrettifyFeatureResult >; export function signalStoreFeature< @@ -79,7 +84,7 @@ export function signalStoreFeature< F1 extends SignalStoreFeatureResult >( input: Input, - f1: SignalStoreFeature, F1> + f1: SignalStoreFeature, F1> ): SignalStoreFeature, F1>; export function signalStoreFeature< Input extends Partial, @@ -87,14 +92,11 @@ export function signalStoreFeature< F2 extends SignalStoreFeatureResult >( input: Input, - f1: SignalStoreFeature, F1>, - f2: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1]>, - F2 - > + f1: SignalStoreFeature, F1>, + f2: SignalStoreFeature & F1, F2> ): SignalStoreFeature< Prettify, - MergeFeatureResults<[F1, F2]> + PrettifyFeatureResult >; export function signalStoreFeature< Input extends Partial, @@ -103,18 +105,12 @@ export function signalStoreFeature< F3 extends SignalStoreFeatureResult >( input: Input, - f1: SignalStoreFeature, F1>, - f2: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1]>, - F2 - >, - f3: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1, F2]>, - F3 - > + f1: SignalStoreFeature, F1>, + f2: SignalStoreFeature & F1, F2>, + f3: SignalStoreFeature & F1 & F2, F3> ): SignalStoreFeature< Prettify, - MergeFeatureResults<[F1, F2, F3]> + PrettifyFeatureResult >; export function signalStoreFeature< Input extends Partial, @@ -124,22 +120,13 @@ export function signalStoreFeature< F4 extends SignalStoreFeatureResult >( Input: Input, - f1: SignalStoreFeature, F1>, - f2: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1]>, - F2 - >, - f3: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1, F2]>, - F3 - >, - f4: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1, F2, F3]>, - F4 - > + f1: SignalStoreFeature, F1>, + f2: SignalStoreFeature & F1, F2>, + f3: SignalStoreFeature & F1 & F2, F3>, + f4: SignalStoreFeature & F1 & F2 & F3, F4> ): SignalStoreFeature< Prettify, - MergeFeatureResults<[F1, F2, F3, F4]> + PrettifyFeatureResult >; export function signalStoreFeature< Input extends Partial, @@ -150,26 +137,14 @@ export function signalStoreFeature< F5 extends SignalStoreFeatureResult >( input: Input, - f1: SignalStoreFeature, F1>, - f2: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1]>, - F2 - >, - f3: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1, F2]>, - F3 - >, - f4: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1, F2, F3]>, - F4 - >, - f5: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1, F2, F3, F4]>, - F5 - > + f1: SignalStoreFeature, F1>, + f2: SignalStoreFeature & F1, F2>, + f3: SignalStoreFeature & F1 & F2, F3>, + f4: SignalStoreFeature & F1 & F2 & F3, F4>, + f5: SignalStoreFeature & F1 & F2 & F3 & F4, F5> ): SignalStoreFeature< Prettify, - MergeFeatureResults<[F1, F2, F3, F4, F5]> + PrettifyFeatureResult >; export function signalStoreFeature< Input extends Partial, @@ -181,32 +156,15 @@ export function signalStoreFeature< F6 extends SignalStoreFeatureResult >( input: Input, - f1: SignalStoreFeature, F1>, - f2: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1]>, - F2 - >, - f3: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1, F2]>, - F3 - >, - f4: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1, F2, F3]>, - F4 - >, - f5: SignalStoreFeature< - MergeFeatureResults<[Prettify, F1, F2, F3, F4]>, - F5 - >, - f6: SignalStoreFeature< - MergeFeatureResults< - [Prettify, F1, F2, F3, F4, F5] - >, - F6 - > + f1: SignalStoreFeature, F1>, + f2: SignalStoreFeature & F1, F2>, + f3: SignalStoreFeature & F1 & F2, F3>, + f4: SignalStoreFeature & F1 & F2 & F3, F4>, + f5: SignalStoreFeature & F1 & F2 & F3 & F4, F5>, + f6: SignalStoreFeature & F1 & F2 & F3 & F4 & F5, F6> ): SignalStoreFeature< Prettify, - MergeFeatureResults<[F1, F2, F3, F4, F5, F6]> + PrettifyFeatureResult >; export function signalStoreFeature( diff --git a/modules/signals/src/signal-store-models.ts b/modules/signals/src/signal-store-models.ts index 32486c1748..240001c031 100644 --- a/modules/signals/src/signal-store-models.ts +++ b/modules/signals/src/signal-store-models.ts @@ -1,10 +1,8 @@ import { Signal } from '@angular/core'; import { DeepSignal } from './deep-signal'; -import { StateSignal } from './state-signal'; +import { StateSource } from './state-source'; import { IsKnownRecord, Prettify } from './ts-helpers'; -export type SignalStoreConfig = { providedIn: 'root' }; - export type StateSignals = IsKnownRecord> extends true ? { [Key in keyof State]: IsKnownRecord extends true @@ -13,13 +11,6 @@ export type StateSignals = IsKnownRecord> extends true } : {}; -export type SignalStoreProps = - Prettify< - StateSignals & - FeatureResult['computed'] & - FeatureResult['methods'] - >; - export type SignalsDictionary = Record>; export type MethodsDictionary = Record; @@ -38,7 +29,7 @@ export type InnerSignalStore< computedSignals: ComputedSignals; methods: Methods; hooks: SignalStoreHooks; -} & StateSignal; +} & StateSource; export type SignalStoreFeatureResult = { state: object; @@ -54,36 +45,3 @@ export type SignalStoreFeature< > = ( store: InnerSignalStore ) => InnerSignalStore; - -export type MergeFeatureResults< - FeatureResults extends SignalStoreFeatureResult[] -> = FeatureResults extends [] - ? EmptyFeatureResult - : FeatureResults extends [infer First extends SignalStoreFeatureResult] - ? First - : FeatureResults extends [ - infer First extends SignalStoreFeatureResult, - infer Second extends SignalStoreFeatureResult - ] - ? MergeTwoFeatureResults - : FeatureResults extends [ - infer First extends SignalStoreFeatureResult, - infer Second extends SignalStoreFeatureResult, - ...infer Rest extends SignalStoreFeatureResult[] - ] - ? MergeFeatureResults<[MergeTwoFeatureResults, ...Rest]> - : never; - -type FeatureResultKeys = - | keyof FeatureResult['state'] - | keyof FeatureResult['computed'] - | keyof FeatureResult['methods']; - -type MergeTwoFeatureResults< - First extends SignalStoreFeatureResult, - Second extends SignalStoreFeatureResult -> = { - state: Omit>; - computed: Omit>; - methods: Omit>; -} & Second; diff --git a/modules/signals/src/signal-store.ts b/modules/signals/src/signal-store.ts index fdfccad04e..be602e1d94 100644 --- a/modules/signals/src/signal-store.ts +++ b/modules/signals/src/signal-store.ts @@ -1,63 +1,70 @@ import { DestroyRef, inject, Injectable, signal, Type } from '@angular/core'; -import { STATE_SIGNAL, StateSignal } from './state-signal'; +import { STATE_SOURCE, StateSource } from './state-source'; import { EmptyFeatureResult, InnerSignalStore, - MergeFeatureResults, - SignalStoreConfig, SignalStoreFeature, SignalStoreFeatureResult, - SignalStoreProps, + StateSignals, } from './signal-store-models'; import { Prettify } from './ts-helpers'; +type SignalStoreConfig = { providedIn: 'root' }; + +type SignalStoreMembers = + Prettify< + StateSignals & + FeatureResult['computed'] & + FeatureResult['methods'] + >; + export function signalStore( f1: SignalStoreFeature -): Type & StateSignal>>; +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults<[F1, F2]> + R extends SignalStoreFeatureResult = F1 & F2 >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2> -): Type & StateSignal>>; +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, F3 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults<[F1, F2, F3]> + R extends SignalStoreFeatureResult = F1 & F2 & F3 >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3> -): Type & StateSignal>>; + f3: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, F3 extends SignalStoreFeatureResult, F4 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults<[F1, F2, F3, F4]> + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, F3 extends SignalStoreFeatureResult, F4 extends SignalStoreFeatureResult, F5 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults<[F1, F2, F3, F4, F5]> + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 & F5 >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -65,17 +72,15 @@ export function signalStore< F4 extends SignalStoreFeatureResult, F5 extends SignalStoreFeatureResult, F6 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults< - [F1, F2, F3, F4, F5, F6] - > + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 & F5 & F6 >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5>, - f6: SignalStoreFeature, F6> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature, + f6: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -84,18 +89,16 @@ export function signalStore< F5 extends SignalStoreFeatureResult, F6 extends SignalStoreFeatureResult, F7 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults< - [F1, F2, F3, F4, F5, F6, F7] - > + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 & F5 & F6 & F7 >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5>, - f6: SignalStoreFeature, F6>, - f7: SignalStoreFeature, F7> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature, + f6: SignalStoreFeature, + f7: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -105,19 +108,17 @@ export function signalStore< F6 extends SignalStoreFeatureResult, F7 extends SignalStoreFeatureResult, F8 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults< - [F1, F2, F3, F4, F5, F6, F7, F8] - > + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 & F5 & F6 & F7 & F8 >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5>, - f6: SignalStoreFeature, F6>, - f7: SignalStoreFeature, F7>, - f8: SignalStoreFeature, F8> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature, + f6: SignalStoreFeature, + f7: SignalStoreFeature, + f8: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -128,76 +129,79 @@ export function signalStore< F7 extends SignalStoreFeatureResult, F8 extends SignalStoreFeatureResult, F9 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults< - [F1, F2, F3, F4, F5, F6, F7, F8, F9] - > + R extends SignalStoreFeatureResult = F1 & + F2 & + F3 & + F4 & + F5 & + F6 & + F7 & + F8 & + F9 >( f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5>, - f6: SignalStoreFeature, F6>, - f7: SignalStoreFeature, F7>, - f8: SignalStoreFeature, F8>, - f9: SignalStoreFeature< - MergeFeatureResults<[F1, F2, F3, F4, F5, F6, F7, F8]>, - F9 - > -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature, + f6: SignalStoreFeature, + f7: SignalStoreFeature, + f8: SignalStoreFeature, + f9: SignalStoreFeature +): Type & StateSource>>; export function signalStore( config: SignalStoreConfig, f1: SignalStoreFeature -): Type & StateSignal>>; +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults<[F1, F2]> + R extends SignalStoreFeatureResult = F1 & F2 >( config: SignalStoreConfig, f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2> -): Type & StateSignal>>; +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, F3 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults<[F1, F2, F3]> + R extends SignalStoreFeatureResult = F1 & F2 & F3 >( config: SignalStoreConfig, f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3> -): Type & StateSignal>>; + f3: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, F3 extends SignalStoreFeatureResult, F4 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults<[F1, F2, F3, F4]> + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 >( config: SignalStoreConfig, f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, F3 extends SignalStoreFeatureResult, F4 extends SignalStoreFeatureResult, F5 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults<[F1, F2, F3, F4, F5]> + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 & F5 >( config: SignalStoreConfig, f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -205,18 +209,16 @@ export function signalStore< F4 extends SignalStoreFeatureResult, F5 extends SignalStoreFeatureResult, F6 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults< - [F1, F2, F3, F4, F5, F6] - > + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 & F5 & F6 >( config: SignalStoreConfig, f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5>, - f6: SignalStoreFeature, F6> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature, + f6: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -225,19 +227,17 @@ export function signalStore< F5 extends SignalStoreFeatureResult, F6 extends SignalStoreFeatureResult, F7 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults< - [F1, F2, F3, F4, F5, F6, F7] - > + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 & F5 & F6 & F7 >( config: SignalStoreConfig, f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5>, - f6: SignalStoreFeature, F6>, - f7: SignalStoreFeature, F7> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature, + f6: SignalStoreFeature, + f7: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -247,20 +247,18 @@ export function signalStore< F6 extends SignalStoreFeatureResult, F7 extends SignalStoreFeatureResult, F8 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults< - [F1, F2, F3, F4, F5, F6, F7, F8] - > + R extends SignalStoreFeatureResult = F1 & F2 & F3 & F4 & F5 & F6 & F7 & F8 >( config: SignalStoreConfig, f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5>, - f6: SignalStoreFeature, F6>, - f7: SignalStoreFeature, F7>, - f8: SignalStoreFeature, F8> -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature, + f6: SignalStoreFeature, + f7: SignalStoreFeature, + f8: SignalStoreFeature +): Type & StateSource>>; export function signalStore< F1 extends SignalStoreFeatureResult, F2 extends SignalStoreFeatureResult, @@ -271,28 +269,31 @@ export function signalStore< F7 extends SignalStoreFeatureResult, F8 extends SignalStoreFeatureResult, F9 extends SignalStoreFeatureResult, - R extends SignalStoreFeatureResult = MergeFeatureResults< - [F1, F2, F3, F4, F5, F6, F7, F8, F9] - > + R extends SignalStoreFeatureResult = F1 & + F2 & + F3 & + F4 & + F5 & + F6 & + F7 & + F8 & + F9 >( config: SignalStoreConfig, f1: SignalStoreFeature, f2: SignalStoreFeature<{} & F1, F2>, - f3: SignalStoreFeature, F3>, - f4: SignalStoreFeature, F4>, - f5: SignalStoreFeature, F5>, - f6: SignalStoreFeature, F6>, - f7: SignalStoreFeature, F7>, - f8: SignalStoreFeature, F8>, - f9: SignalStoreFeature< - MergeFeatureResults<[F1, F2, F3, F4, F5, F6, F7, F8]>, - F9 - > -): Type & StateSignal>>; + f3: SignalStoreFeature, + f4: SignalStoreFeature, + f5: SignalStoreFeature, + f6: SignalStoreFeature, + f7: SignalStoreFeature, + f8: SignalStoreFeature, + f9: SignalStoreFeature +): Type & StateSource>>; export function signalStore( ...args: [SignalStoreConfig, ...SignalStoreFeature[]] | SignalStoreFeature[] -): Type> { +): Type> { const signalStoreArgs = [...args]; const config: Partial = @@ -309,12 +310,12 @@ export function signalStore( getInitialInnerStore() ); const { stateSignals, computedSignals, methods, hooks } = innerStore; - const props = { ...stateSignals, ...computedSignals, ...methods }; + const storeMembers = { ...stateSignals, ...computedSignals, ...methods }; - (this as any)[STATE_SIGNAL] = innerStore[STATE_SIGNAL]; + (this as any)[STATE_SOURCE] = innerStore[STATE_SOURCE]; - for (const key in props) { - (this as any)[key] = props[key]; + for (const key in storeMembers) { + (this as any)[key] = storeMembers[key]; } const { onInit, onDestroy } = hooks; @@ -334,7 +335,7 @@ export function signalStore( export function getInitialInnerStore(): InnerSignalStore { return { - [STATE_SIGNAL]: signal({}), + [STATE_SOURCE]: signal({}), stateSignals: {}, computedSignals: {}, methods: {}, diff --git a/modules/signals/src/state-signal.ts b/modules/signals/src/state-signal.ts deleted file mode 100644 index 2aca78db0c..0000000000 --- a/modules/signals/src/state-signal.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { WritableSignal } from '@angular/core'; - -export const STATE_SIGNAL = Symbol('STATE_SIGNAL'); - -export type StateSignal = { - [STATE_SIGNAL]: WritableSignal; -}; diff --git a/modules/signals/src/patch-state.ts b/modules/signals/src/state-source.ts similarity index 54% rename from modules/signals/src/patch-state.ts rename to modules/signals/src/state-source.ts index 629d4bf2f9..75d8632bf8 100644 --- a/modules/signals/src/patch-state.ts +++ b/modules/signals/src/state-source.ts @@ -1,17 +1,23 @@ -import { STATE_SIGNAL, StateSignal } from './state-signal'; +import { WritableSignal } from '@angular/core'; import { Prettify } from './ts-helpers'; +export const STATE_SOURCE = Symbol('STATE_SOURCE'); + +export type StateSource = { + [STATE_SOURCE]: WritableSignal; +}; + export type PartialStateUpdater = ( state: State ) => Partial; export function patchState( - stateSignal: StateSignal, + stateSource: StateSource, ...updaters: Array< Partial> | PartialStateUpdater> > ): void { - stateSignal[STATE_SIGNAL].update((currentState) => + stateSource[STATE_SOURCE].update((currentState) => updaters.reduce( (nextState: State, updater) => ({ ...nextState, @@ -21,3 +27,9 @@ export function patchState( ) ); } + +export function getState( + stateSource: StateSource +): State { + return stateSource[STATE_SOURCE](); +} diff --git a/modules/signals/src/with-computed.ts b/modules/signals/src/with-computed.ts index 0245c8363f..a2abedca3c 100644 --- a/modules/signals/src/with-computed.ts +++ b/modules/signals/src/with-computed.ts @@ -1,6 +1,5 @@ -import { excludeKeys } from './helpers'; +import { assertUniqueStoreMembers } from './signal-store-assertions'; import { - EmptyFeatureResult, InnerSignalStore, SignalsDictionary, SignalStoreFeature, @@ -18,22 +17,18 @@ export function withComputed< ) => ComputedSignals ): SignalStoreFeature< Input, - EmptyFeatureResult & { computed: ComputedSignals } + { state: {}; computed: ComputedSignals; methods: {} } > { return (store) => { const computedSignals = signalsFactory({ ...store.stateSignals, ...store.computedSignals, }); - const computedSignalsKeys = Object.keys(computedSignals); - const stateSignals = excludeKeys(store.stateSignals, computedSignalsKeys); - const methods = excludeKeys(store.methods, computedSignalsKeys); + assertUniqueStoreMembers(store, Object.keys(computedSignals)); return { ...store, - stateSignals, computedSignals: { ...store.computedSignals, ...computedSignals }, - methods, } as InnerSignalStore, ComputedSignals>; }; } diff --git a/modules/signals/src/with-hooks.ts b/modules/signals/src/with-hooks.ts index af7fbeb5d3..c2ead13aec 100644 --- a/modules/signals/src/with-hooks.ts +++ b/modules/signals/src/with-hooks.ts @@ -1,4 +1,4 @@ -import { STATE_SIGNAL, StateSignal } from './state-signal'; +import { STATE_SOURCE, StateSource } from './state-source'; import { EmptyFeatureResult, SignalStoreFeature, @@ -12,7 +12,7 @@ type HookFn = ( StateSignals & Input['computed'] & Input['methods'] & - StateSignal> + StateSource> > ) => void; @@ -21,7 +21,7 @@ type HooksFactory = ( StateSignals & Input['computed'] & Input['methods'] & - StateSignal> + StateSource> > ) => { onInit?: () => void; @@ -45,15 +45,15 @@ export function withHooks( | HooksFactory ): SignalStoreFeature { return (store) => { - const storeProps = { - [STATE_SIGNAL]: store[STATE_SIGNAL], + const storeMembers = { + [STATE_SOURCE]: store[STATE_SOURCE], ...store.stateSignals, ...store.computedSignals, ...store.methods, }; const hooks = typeof hooksOrFactory === 'function' - ? hooksOrFactory(storeProps) + ? hooksOrFactory(storeMembers) : hooksOrFactory; const createHook = (name: keyof typeof hooks) => { const hook = hooks[name]; @@ -65,7 +65,7 @@ export function withHooks( currentHook(); } - hook(storeProps); + hook(storeMembers); } : currentHook; }; diff --git a/modules/signals/src/with-methods.ts b/modules/signals/src/with-methods.ts index e96741e40a..cb13061dbd 100644 --- a/modules/signals/src/with-methods.ts +++ b/modules/signals/src/with-methods.ts @@ -1,7 +1,6 @@ -import { excludeKeys } from './helpers'; -import { STATE_SIGNAL, StateSignal } from './state-signal'; +import { STATE_SOURCE, StateSource } from './state-source'; +import { assertUniqueStoreMembers } from './signal-store-assertions'; import { - EmptyFeatureResult, InnerSignalStore, MethodsDictionary, SignalsDictionary, @@ -20,25 +19,21 @@ export function withMethods< StateSignals & Input['computed'] & Input['methods'] & - StateSignal> + StateSource> > ) => Methods -): SignalStoreFeature { +): SignalStoreFeature { return (store) => { const methods = methodsFactory({ - [STATE_SIGNAL]: store[STATE_SIGNAL], + [STATE_SOURCE]: store[STATE_SOURCE], ...store.stateSignals, ...store.computedSignals, ...store.methods, }); - const methodsKeys = Object.keys(methods); - const stateSignals = excludeKeys(store.stateSignals, methodsKeys); - const computedSignals = excludeKeys(store.computedSignals, methodsKeys); + assertUniqueStoreMembers(store, Object.keys(methods)); return { ...store, - stateSignals, - computedSignals, methods: { ...store.methods, ...methods }, } as InnerSignalStore, SignalsDictionary, Methods>; }; diff --git a/modules/signals/src/with-state.ts b/modules/signals/src/with-state.ts index 64c26c73b3..2d116a2395 100644 --- a/modules/signals/src/with-state.ts +++ b/modules/signals/src/with-state.ts @@ -1,7 +1,7 @@ import { computed } from '@angular/core'; +import { assertUniqueStoreMembers } from './signal-store-assertions'; import { toDeepSignal } from './deep-signal'; -import { excludeKeys } from './helpers'; -import { STATE_SIGNAL } from './state-signal'; +import { STATE_SOURCE } from './state-source'; import { EmptyFeatureResult, InnerSignalStore, @@ -14,44 +14,42 @@ export function withState( stateFactory: () => State ): SignalStoreFeature< EmptyFeatureResult, - EmptyFeatureResult & { state: State } + { state: State; computed: {}; methods: {} } >; export function withState( state: State ): SignalStoreFeature< EmptyFeatureResult, - EmptyFeatureResult & { state: State } + { state: State; computed: {}; methods: {} } >; export function withState( stateOrFactory: State | (() => State) ): SignalStoreFeature< SignalStoreFeatureResult, - EmptyFeatureResult & { state: State } + { state: State; computed: {}; methods: {} } > { return (store) => { const state = typeof stateOrFactory === 'function' ? stateOrFactory() : stateOrFactory; const stateKeys = Object.keys(state); - store[STATE_SIGNAL].update((currentState) => ({ + assertUniqueStoreMembers(store, stateKeys); + + store[STATE_SOURCE].update((currentState) => ({ ...currentState, ...state, })); const stateSignals = stateKeys.reduce((acc, key) => { const sliceSignal = computed( - () => (store[STATE_SIGNAL]() as Record)[key] + () => (store[STATE_SOURCE]() as Record)[key] ); return { ...acc, [key]: toDeepSignal(sliceSignal) }; }, {} as SignalsDictionary); - const computedSignals = excludeKeys(store.computedSignals, stateKeys); - const methods = excludeKeys(store.methods, stateKeys); return { ...store, stateSignals: { ...store.stateSignals, ...stateSignals }, - computedSignals, - methods, } as InnerSignalStore; }; }