potential degradation of withMethods generic types inference
Which @ngrx/* package(s) are the source of the bug?
signals
Minimal reproduction of the bug/regression with instructions
I was trying to create a "generic" signal store with generic state type and generic methods. Example:
interface Book {
id: number
title: string
author: string
}
function EntityFilter<T extends object>(properties: (keyof T)[]) {
const initial = Object.fromEntries(
properties.map(prop => [prop, null])
) as { [K in keyof T]: T[K] | null }
return signalStore(
withState(initial),
withMethods(store => ({
updateFilter<K extends keyof T>(property: K, value: T[K]){
patchState(store, (state) => ({
...state,
[property]: value
}));
}
}))
)
}
const BookFilter = EntityFilter<Book>(['id', 'title', 'author'])
const bookFilters = inject(BookFilter);
bookFilters.title() // OK, string
bookFilters.author() // OK, string
bookFilters.updateFilter('title', 'The Hobbit') // OK
bookFilters.updateFilter('title', 123) // ❌ should fail - 123 is not a string
bookFilters.updateFilter('xxx', 123) // ❌ should fail - 'xxx' is not a property of Book
Everything compiles, but last 2 line SHOULD NOT, but they do.
The issue is on the EntityFilter: updateFilter<any>(property: any, value: T[any]): void;
Although K extends keyof T is the super standard way to define a generic method parameter type, it somehow gets lost throughout the type inference process.
As a result, the fields/signals (output of withState) are perfectly correctly typed (guess that was much easier to cover with types). But the methods (output of withMethods) gets any instead of keyof T.
This stands in the way of creating signal stores without explicitly using type assertions (BookFilter as I_WILL_TELL_YOU_WHAT_IT_IS), which should be generally avoided.
Expected behavior
I would expect
bookFilters.updateFilter('title', 123) // ❌ should fail - 123 is not a string
bookFilters.updateFilter('xxx', 123) // ❌ should fail - 'xxx' is not a property of Book
to fail, but it doesn't
Versions of NgRx, Angular, Node, affected browser(s) and operating system(s)
ngrx: ^17.1.0
Other information
No response
I would be willing to submit a PR to fix this issue
- [X] Not sure
It seems that SignalStore pushes the limits of TypeScript. 😅 I'll try to find a fix for this issue. In the meantime, you can use the following workaround:
function EntityFilter<T extends object>(properties: (keyof T)[]) {
const initial = Object.fromEntries(
properties.map(prop => [prop, null])
) as { [K in keyof T]: T[K] | null }
const entityFilterMethods = withMethods(store => ({
updateFilter<K extends keyof T>(property: K, value: T[K]){
patchState(store, (state) => ({
...state,
[property]: value
}));
}
}));
return signalStore(withState(initial), entityFilterMethods);
}
By moving withMethods outside of the signalStore chain, the updateFilter method type will be properly inferred.
@markostanimirovic
I would also like to join this discussion because our issue goes in the same direction but I am not sure it is a TypeScript or SignalStore limitation.
Minimal reproduction of the bug/regression with instructions:
I was trying to create a "generic" signalStoreFeature factory with generic state type and generic methods.
Example:
export const createWithRequest = <RestEndpoint Record<string, string>>(
restEndpoints: RestEndpoint,
) => ({
withRequest: signalStoreFeatureFactory(restEndpoints),
});
export function signalStoreFeatureFactory<RestEndpoint extends Record<string, string>>(
restEndpoints: RestEndpoint,
) {
return () =>
signalStoreFeature(
withMethods(store => ({
request: <Method extends keyof RestEndpoint & string>(method: Method) => {
patchState(store, ...);
},
})),
);
}
Usage:
type RestEndpoints = {
get: {
'meetings': Meeting[] | null;
};
};
const requestState: RestEndpoints = {
get: {
meetings: null,
},
};
export const { withRequest } = createWithRequest(requestState);
export const SomeStore = signalStore(
withRequest(),
withMethods(store => ({
foo() {
store.request('getttt'); // it should fail but somehow it is `any` type
},
})),
);
@markostanimirovic
it seems your fix in https://github.com/ngrx/platform/pull/4249 (export type MethodsDictionary = Record<string, Function>;) solved it 🎉
Its there any workaround in the meantime for it?
I dont know when the next release (minor, patch) for NgRx is but there is an npm package patch lib which can patch 3rd party libs in node_modules https://dev.to/zhnedyalkow/the-easiest-way-to-patch-your-npm-package-4ece
BTW: many thanks to the NgRx team and especially to you. The SignalStore is incredibly powerful and extendable.
NgRx v17.1.1 is released today ✅