Add types to a function that combines NgRX store selectors

32 Views Asked by At

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();
                    });
            });
});
0

There are 0 best solutions below