Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: produced keys should be readonly (typely and technically)
Browse files Browse the repository at this point in the history
arturovt committed Mar 24, 2024

Verified

This commit was signed with the committer’s verified signature.
jalaziz Jameel Al-Aziz
1 parent 6037e1d commit d111778
Showing 5 changed files with 44 additions and 15 deletions.
11 changes: 7 additions & 4 deletions packages/signals/src/produce-actions.ts
Original file line number Diff line number Diff line change
@@ -9,16 +9,19 @@ export type ActionMap = Record<string, ActionDef>;
export function produceActions<T extends ActionMap>(actionMap: RequireAtLeastOneProperty<T>) {
const store = inject(Store);

return Object.fromEntries(
Object.entries(actionMap).map(([key, actionType]) => [key, createDispatchFn(actionType)])
) as unknown as {
return Object.entries(actionMap).reduce((accumulator, [key, actionType]) => {
Object.defineProperty(accumulator, key, {
value: createDispatchFn(actionType)
});
return accumulator;
}, {}) as {
// This is inlined to enhance developer experience.
// If we were to switch to another type, such as
// `type ActionMapReturnType<T extends ActionMap> = { ... }`, code editors
// would display the return type simply as `ActionMapReturnType`, rather
// than presenting it as an object with properties that correspond to
// functions returning observables.
[K in keyof T]: (...args: ConstructorParameters<T[K]>) => Observable<void>;
readonly [K in keyof T]: (...args: ConstructorParameters<T[K]>) => Observable<void>;
};

function createDispatchFn(actionType: ActionDef) {
11 changes: 7 additions & 4 deletions packages/signals/src/produce-selectors.ts
Original file line number Diff line number Diff line change
@@ -10,15 +10,18 @@ export function produceSelectors<T extends SelectorMap>(
) {
const store = inject(Store);

return Object.fromEntries(
Object.entries(selectorMap).map(([key, selector]) => [key, store.selectSignal(selector)])
) as {
return Object.entries(selectorMap).reduce((accumulator, [key, selector]) => {
Object.defineProperty(accumulator, key, {
value: store.selectSignal(selector)
});
return accumulator;
}, {}) as {
// This is inlined to enhance developer experience.
// If we were to switch to another type, such as
// `type SelectorMapReturnType<T extends SelectorMap> = { ... }`, code editors
// would display the return type simply as `SelectorMapReturnType`, rather
// than presenting it as an object with properties that correspond to
// signals that keep store selected value.
[K in keyof T]: Signal<ɵSelectorReturnType<T[K]>>;
readonly [K in keyof T]: Signal<ɵSelectorReturnType<T[K]>>;
};
}
23 changes: 23 additions & 0 deletions packages/signals/tests/produce-selectors.spec.ts
Original file line number Diff line number Diff line change
@@ -36,4 +36,27 @@ describe('produceSelectors', () => {
// Assert
expect(selectors.counter()).toEqual(0);
});

it('should produce a readonly property', () => {
// Arrange
testSetup();

// Act
const selectors = runInInjectionContext(TestBed, () =>
produceSelectors({
counter: CounterState.getCounter
})
);

let message: string | null = null;

try {
(selectors as any).counter = {};
} catch (error) {
message = error.message;
}

// Assert
expect(message).toContain('Cannot assign to read only property');
});
});
4 changes: 2 additions & 2 deletions packages/signals/types/tests/produce-actions.lint.ts
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ describe('[TEST]: produceActions', () => {
constructor(readonly name_1: string, readonly name_2: number) {}
}

produceActions({ action: ValidAction }); // $ExpectType { action: (name: string) => Observable<void>; }
produceActions({ action: ValidAction, action_2: ValidActionWithMultipleParameters }); // $ExpectType { action: (name: string) => Observable<void>; action_2: (name_1: string, name_2: number) => Observable<void>; }
produceActions({ action: ValidAction }); // $ExpectType { readonly action: (name: string) => Observable<void>; }
produceActions({ action: ValidAction, action_2: ValidActionWithMultipleParameters }); // $ExpectType { readonly action: (name: string) => Observable<void>; readonly action_2: (name_1: string, name_2: number) => Observable<void>; }
});
});
10 changes: 5 additions & 5 deletions packages/signals/types/tests/produce-selectors.lint.ts
Original file line number Diff line number Diff line change
@@ -31,15 +31,15 @@ describe('[TEST]: produceSelectors', () => {
});

it('should infer correct return types', () => {
produceSelectors({ counter: CounterState.getCounter }); // $ExpectType { counter: Signal<CounterStateModel>; }
produceSelectors({ counter: COUNTER_STATE_TOKEN }); // $ExpectType { counter: Signal<CounterStateModel>; }
produceSelectors({ counter: CounterState.getCounter }); // $ExpectType { readonly counter: Signal<CounterStateModel>; }
produceSelectors({ counter: COUNTER_STATE_TOKEN }); // $ExpectType { readonly counter: Signal<CounterStateModel>; }

const dynamicSelector_withStateClass = createSelector([CounterState], state => state.counter);
const dynamicSelector_withSelector = createSelector([CounterState.getCounter], state => state.counter);
const dynamicSelector_withStateToken = createSelector([COUNTER_STATE_TOKEN], state => state.counter);

produceSelectors({ counter: dynamicSelector_withStateClass }); // $ExpectType { counter: Signal<any>; }
produceSelectors({ counter: dynamicSelector_withSelector }); // $ExpectType { counter: Signal<number>; }
produceSelectors({ counter: dynamicSelector_withStateToken }); // $ExpectType { counter: Signal<number>; }
produceSelectors({ counter: dynamicSelector_withStateClass }); // $ExpectType { readonly counter: Signal<any>; }
produceSelectors({ counter: dynamicSelector_withSelector }); // $ExpectType { readonly counter: Signal<number>; }
produceSelectors({ counter: dynamicSelector_withStateToken }); // $ExpectType { readonly counter: Signal<number>; }
});
});

0 comments on commit d111778

Please sign in to comment.