Creating a selector factory with createSelector in TypeScript

Creating selectors like this:

import { createSelector } from 'reselect';

export interface Node {
  nodeId: number
  nodeName: string
}

export type NodeState = {
  nodes: Node[];
  text: string;
};

const nodeListState = (state) => state.nodeList;


const byKey = (key: keyof NodeState) => createSelector(
  nodeListState, (nodeList: NodeState) => nodeList[key],
);

export const getNodes = byKey('nodes');
export const getText = byKey('text');

Elsewhere, using the selectors:

import { useSelector } from 'react-redux';

const nodes = useSelector(selectors.getNodes);

nodes.map(...)

This results in the error:

Property 'map' does not exist on type 'string | Node[]'.
  Property 'map' does not exist on type 'string'.  TS2339

The variable nodes is actually an array. Am I going about this all wrong? What’s the correct way to set up a function that creates selectors by key in TS?

Answer

What’s happening here is that typescript is not able to distinguish between byKey('nodes') and byKey('text'). They both return the same type, which is a selector that selects either the text string or the nodes Node[]. Thus const nodes = useSelector(selectors.getNodes) returns the union string | Node[] and you get an error that string is not an array.

One way to fix this is to ditch byKey and create the two selectors separately.

But we can make byKey work properly by making it a generic function which depends on the specific key that it was called with. That way we know that byKey('nodes') selects the 'nodes' property.

If you apply proper typing to nodeListState, you don’t actually need to specify the argument for the second selector as nodeList: NodeState since it can be inferred based on the return type of the first selector.

const nodeListState = (state: {nodeList: NodeState}) => state.nodeList;

const byKey = <K extends keyof NodeState>(key: K) => createSelector(
  nodeListState, 
  (nodeList) => nodeList[key],
);

Now getNodes selects Node[] and getText selects string.