Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions modules/signals/spec/readonly-issue-demo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expecter } from 'ts-snippet';
import { compilerOptions } from './types/helpers';

describe('signalStore readonly members', () => {
const expectSnippet = expecter(
(code) => `
import { computed, signal, Signal } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';

const UserStore = signalStore(
withState<{ name: string; age: number }>({ name: 'Alice', age: 30 }),
withComputed((store) => ({
greeting: computed(() => 'Hi, ' + store.name()),
})),
withMethods((store) => ({
setName(name: string) { patchState(store, { name }); },
}))
);

const store = new UserStore();
${code}
`,
compilerOptions()
);

it('prevents reassigning state signals', () => {
expectSnippet(`store.name = signal('x') as unknown as Signal<string>;`).toFail(/read-only/);
});

it('prevents reassigning computed signals', () => {
expectSnippet(`store.greeting = signal('x') as unknown as Signal<string>;`).toFail(/read-only/);
});

it('prevents reassigning methods', () => {
expectSnippet(`store.setName = (n: string) => {};`).toFail(/read-only/);
});
});
32 changes: 16 additions & 16 deletions modules/signals/spec/types/signal-store.types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'Store',
'Type<{ foo: Signal<string>; bar: Signal<number[]>; } & StateSource<{ foo: string; bar: number[]; }>>'
'Type<{ readonly foo: Signal<string>; readonly bar: Signal<number[]>; } & StateSource<{ foo: string; bar: number[]; }>>'
);
});

Expand Down Expand Up @@ -62,7 +62,7 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'store',
'{ user: DeepSignal<{ age: number; details: { first: string; flags: boolean[]; }; }>; } & StateSource<{ user: { age: number; details: { first: string; flags: boolean[]; }; }; }>'
'{ readonly user: DeepSignal<{ age: number; details: { first: string; flags: boolean[]; }; }>; } & StateSource<{ user: { age: number; details: { first: string; flags: boolean[]; }; }; }>'
);
result.toInfer(
'user',
Expand Down Expand Up @@ -240,7 +240,7 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'store',
'{ foo: Signal<number | { s: string; }>; bar: DeepSignal<{ baz: { b: boolean; } | null; }>; x: DeepSignal<{ y: { z: number | undefined; }; }>; } & StateSource<{ foo: number | { ...; }; bar: { ...; }; x: { ...; }; }>'
'{ readonly foo: Signal<number | { s: string; }>; readonly bar: DeepSignal<{ baz: { b: boolean; } | null; }>; readonly x: DeepSignal<{ y: { z: number | undefined; }; }>; } & StateSource<...>'
);
result.toInfer('foo', 'Signal<number | { s: string; }>');
result.toInfer('bar', 'DeepSignal<{ baz: { b: boolean; } | null; }>');
Expand All @@ -264,7 +264,7 @@ describe('signalStore', () => {
const result1 = expectSnippet(snippet1);
result1.toInfer(
'Store',
'Type<{ name: DeepSignal<{ x: { y: string; }; }>; arguments: Signal<number[]>; call: Signal<boolean>; } & StateSource<{ name: { x: { y: string; }; }; arguments: number[]; call: boolean; }>>'
'Type<{ readonly name: DeepSignal<{ x: { y: string; }; }>; readonly arguments: Signal<number[]>; readonly call: Signal<boolean>; } & StateSource<{ name: { x: { y: string; }; }; arguments: number[]; call: boolean; }>>'
);

const snippet2 = `
Expand All @@ -280,7 +280,7 @@ describe('signalStore', () => {
const result2 = expectSnippet(snippet2);
result2.toInfer(
'Store',
'Type<{ apply: Signal<string>; bind: DeepSignal<{ foo: string; }>; prototype: Signal<string[]>; } & StateSource<{ apply: string; bind: { foo: string; }; prototype: string[]; }>>'
'Type<{ readonly apply: Signal<string>; readonly bind: DeepSignal<{ foo: string; }>; readonly prototype: Signal<string[]>; } & StateSource<{ apply: string; bind: { foo: string; }; prototype: string[]; }>>'
);

const snippet3 = `
Expand All @@ -295,7 +295,7 @@ describe('signalStore', () => {
const result3 = expectSnippet(snippet3);
result3.toInfer(
'Store',
'Type<{ length: Signal<number>; caller: Signal<undefined>; } & StateSource<{ length: number; caller: undefined; }>>'
'Type<{ readonly length: Signal<number>; readonly caller: Signal<undefined>; } & StateSource<{ length: number; caller: undefined; }>>'
);
});

Expand Down Expand Up @@ -343,7 +343,7 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'store',
'{ bar: DeepSignal<{ baz?: number | undefined; }>; x: DeepSignal<{ y?: { z: boolean; } | undefined; }>; } & StateSource<{ bar: { baz?: number | undefined; }; x: { y?: { z: boolean; } | undefined; }; }>'
'{ readonly bar: DeepSignal<{ baz?: number | undefined; }>; readonly x: DeepSignal<{ y?: { z: boolean; } | undefined; }>; } & StateSource<{ bar: { baz?: number | undefined; }; x: { ...; }; }>'
);
result.toInfer('bar', 'DeepSignal<{ baz?: number | undefined; }>');
result.toInfer('baz', 'Signal<number | undefined> | undefined');
Expand Down Expand Up @@ -433,12 +433,12 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'store1',
'{ count: Signal<number>; } & StateSource<{ count: number; }>'
'{ readonly count: Signal<number>; } & StateSource<{ count: number; }>'
);
result.toInfer('state1', '{ count: number; }');
result.toInfer(
'store2',
'{ count: Signal<number>; } & StateSource<{ count: number; }>'
'{ readonly count: Signal<number>; } & StateSource<{ count: number; }>'
);
result.toInfer('state2', '{ count: number; }');

Expand Down Expand Up @@ -474,12 +474,12 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'store1',
'{ count: Signal<number>; } & StateSource<{ count: number; }>'
'{ readonly count: Signal<number>; } & StateSource<{ count: number; }>'
);
result.toInfer('state1', '{ count: number; }');
result.toInfer(
'store2',
'{ count: Signal<number>; } & StateSource<{ count: number; }>'
'{ readonly count: Signal<number>; } & StateSource<{ count: number; }>'
);
result.toInfer('state2', '{ count: number; }');

Expand Down Expand Up @@ -515,12 +515,12 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'store1',
'{ count: Signal<number>; } & WritableStateSource<{ count: number; }>'
'{ readonly count: Signal<number>; } & WritableStateSource<{ count: number; }>'
);
result.toInfer('state1', '{ count: number; }');
result.toInfer(
'store2',
'{ count: Signal<number>; } & WritableStateSource<{ count: number; }>'
'{ readonly count: Signal<number>; } & WritableStateSource<{ count: number; }>'
);
result.toInfer('state2', '{ count: number; }');

Expand Down Expand Up @@ -680,7 +680,7 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'store',
'{ ngrx: Signal<string>; x: DeepSignal<{ y: string; }>; signals: Signal<number[]>; mgmt: (arg: boolean) => number; } & StateSource<{ ngrx: string; x: { y: string; }; }>'
'{ readonly ngrx: Signal<string>; readonly x: DeepSignal<{ y: string; }>; readonly signals: Signal<number[]>; readonly mgmt: (arg: boolean) => number; } & StateSource<...>'
);
});

Expand All @@ -707,7 +707,7 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'store',
'{ foo: Signal<number>; bar: Signal<string>; baz: (x: number) => void; } & StateSource<{ foo: number; }>'
'{ readonly foo: Signal<number>; readonly bar: Signal<string>; readonly baz: (x: number) => void; } & StateSource<{ foo: number; }>'
);
});

Expand Down Expand Up @@ -754,7 +754,7 @@ describe('signalStore', () => {
const result = expectSnippet(snippet);
result.toInfer(
'store',
'{ count1: Signal<number>; doubleCount2: Signal<number>; increment1: () => void; } & StateSource<{ count1: number; }>'
'{ readonly count1: Signal<number>; readonly doubleCount2: Signal<number>; readonly increment1: () => void; } & StateSource<{ count1: number; }>'
);
});

Expand Down
10 changes: 6 additions & 4 deletions modules/signals/src/signal-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ type SignalStoreConfig = ProvidedInConfig & { protectedState?: boolean };

type SignalStoreMembers<FeatureResult extends SignalStoreFeatureResult> =
Prettify<
OmitPrivate<
StateSignals<FeatureResult['state']> &
FeatureResult['props'] &
FeatureResult['methods']
Readonly<
OmitPrivate<
StateSignals<FeatureResult['state']> &
FeatureResult['props'] &
FeatureResult['methods']
>
>
>;

Expand Down
4 changes: 2 additions & 2 deletions modules/signals/testing/spec/types/uprotected.types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('unprotected', () => {

expectSnippet(snippet).toInfer(
'unprotectedStore',
'{ count: Signal<number>; doubleCount: Signal<number>; [STATE_SOURCE]: { count: WritableSignal<number>; }; }'
'{ readonly count: Signal<number>; readonly doubleCount: Signal<number>; [STATE_SOURCE]: { count: WritableSignal<number>; }; }'
);
});

Expand All @@ -45,7 +45,7 @@ describe('unprotected', () => {

expectSnippet(snippet).toInfer(
'unprotectedStore',
'{ count: Signal<number>; [STATE_SOURCE]: { count: WritableSignal<number>; }; }'
'{ readonly count: Signal<number>; [STATE_SOURCE]: { count: WritableSignal<number>; }; }'
);
});
}, 8_000);