I work with NgRX and I need help with adding types to a function. I want to create a function combinedSelectorObj that takes an object with any keys and store selectors being their values, for example:
{
size: selectSize(action.id),
color: selectColor(action.id),
}
That function should return an object with the same keys and the observable values from these selectors, e.g.:
{
size: { width: 15, height: 40 },
color: 'blue',
}
In other words, I want these two calls a kind of equivalent:
// 1
{
size: store.select(selectSize(action.id)),
color: store.select(selectColor(action.id)),
)
// 2
store.select(
combinedSelectorObj({
size: selectSize(action.id),
color: selectColor(action.id),
})
)
My problem is that I don't know how to add the types to the function that so intellisense works - the return type is an object with correct keys but all values are any. It was simple with a function that returned an array instead of an object:
function combinedSelectorArray<State, S1, S2>(
s1: Selector<State, S1>,
s2: Selector<State, S2>
): MemoizedSelector<State, [S1, S2]>;
but I can't add types to combinedSelectorObj.
Below is the full code for experimenting (e.g. in VSCode).
import { MemoizedSelector, Selector, Store, createSelector } from '@ngrx/store';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { first, of } from 'rxjs';
import { TestBed } from '@angular/core/testing';
import { concatLatestFrom } from '@ngrx/effects';
// store
type StoreType = {
data: { size: { width: number; height: number }; color: string }[];
};
const initialState: StoreType = {
data: [
{
size: { width: 15, height: 40 },
color: 'blue',
},
],
};
// selectors
const selectSize = (id: number) =>
createSelector(
(state: StoreType) => state.data[id],
(data) => data.size
);
const selectColor = (id: number) =>
createSelector(
(state: StoreType) => state.data[id],
(data) => data.color
);
// Function Under Test.
// It takes an object with any keys and store selectors being their values,
// and it should return an object with the same keys and the values from these selectors.
function combinedSelectorObj<
State,
U extends Record<string, any>,
T extends { [K in keyof U]: Selector<State, U[K]> }
>(
selectors: T
): MemoizedSelector<
State,
{
[K in keyof T & keyof U]: U[K];
}
> {
const keys = Object.keys(selectors);
const values = keys.map((k) => selectors[k]);
const count = keys.length;
if (count === 2) {
return <any>createSelector(values[0], values[1], (...args) => ({
[keys[0]]: args[0],
[keys[1]]: args[1],
}));
}
throw new Error();
}
function combinedSelectorArr<State, S1, S2>(
s1: Selector<State, S1>,
s2: Selector<State, S2>
): MemoizedSelector<State, [S1, S2]> {
return createSelector(s1, s2, (...args) => [...args]);
}
// test case, to be used in IDE only without running to see if it detects types
describe('Test', () => {
// can't use MockStore here as it does not preserve types in store.select()
let store: Store<StoreType>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideMockStore({ initialState })],
});
store = TestBed.inject(MockStore);
});
it('Types for store', (done) => {
of({ type: 'Action 1', id: 0 })
.pipe(
concatLatestFrom((action) =>
store.select(selectSize(action.id)).pipe(first())
)
)
.subscribe(([action, data]) => {
/* IDE tells me that data: {
width: number;
height: number;
}
and width1 below is an error */
console.log(action, data.width, data.width1);
done();
});
});
it('Types for combinedSelectorObj', (done) => {
of({ type: 'Action 1', id: 0 })
.pipe(
concatLatestFrom((action) =>
store
.select(
combinedSelectorObj({
size: selectSize(action.id),
color: selectColor(action.id),
})
)
.pipe(first())
)
)
.subscribe(([action, data]) => {
/* IDE tells me that data: {
size: any;
color: any;
}
and there are no errors below */
console.log(action, data.size.width, data.size.width1);
done();
});
it('Types for combinedSelectorArr', (done) => {
of({ type: 'Action 1', id: 0 })
.pipe(
concatLatestFrom((action) =>
store
.select(
combinedSelectorArr(
selectSize(action.id),
selectColor(action.id),
)
)
.pipe(first())
)
)
.subscribe(([action, data]) => {
/* IDE tells me that data: [{
width: number;
height: number;
}, string]
and width1 below is an error */
console.log(action, data[0].width, data[0].width1);
done();
});
});
});