Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TypedCreateSelector interface to support creating a pre-typed version of createSelector #500

Closed

Conversation

ICodeMyOwnLife
Copy link

@ICodeMyOwnLife ICodeMyOwnLife commented Jun 28, 2021

Similar to TypedUseSelectorHook, this interface allows us to easily create a version of createSelector that is properly typed for our store's root state.

Code example:

import { createSelector, TypedCreateSelector } from 'reselect'
import type { RootState } from './store'

export const createAppSelector: TypedCreateSelector<RootState> = createSelector

This frees us from having to type selectors all the time in some codebase:

Instead of

const selector1 = createSelector((state: RootState) => state.users, ...)
const selector2 = createSelector((state: RootState) => state.roles, ...)

Now we can do this and it's still type-safe:

const selector1 = createSelector(state => state.users, ...)
const selector2 = createSelector(state => state.roles, ...)

There is also a question on this feature on StackOverflow: How to define a version of createSelector typed for my app?

In this PR I converted type of createSelector into an interface so that CreateSelector and TypedCreateSelector have very similar interface structures. I think this will make it easier to update these types.

interface CreateSelector {
  <S, R1, T>(
    selector1: Selector<S, R1>,
    combiner: (res1: R1) => T
  ): OutputSelector<S, T, (res1: R1) => T>;
  ...
}

interface TypedCreateSelector<S> {
  <R1, T>(
    selector1: Selector<S, R1>,
    combiner: (res1: R1) => T
  ): OutputSelector<S, T, (res1: R1) => T>;
  ...
}

I also added typing tests for the new interface.

@nickserv
Copy link
Contributor

Would this still be compatible with old types when not giving the state?

@ICodeMyOwnLife
Copy link
Author

ICodeMyOwnLife commented Jul 13, 2021

Would this still be compatible with old types when not giving the state?

From the view of library consumer this change only adds a new utility type and does not change the old types.
This change passed all old typing tests.
I also have tested it (by replacing the declaration file in node_modules with this new file and running type-check) against projects of my organization and have seen that it didn't introduce any regression. Those projects are enormous codebases with wide use of createSelector.
So I'm quite sure that this would be compatible with old types.

Before, people do this

// someSelector.ts
import { createSelector } from 'reselect';
import { RootState } from 'somewhere';
const someSelector = createSelector((state: AppState) => someState, someCombiner);

After, people can still do as before, but they have one more option

// utils.ts
import { TypedCreateSelector, createSelector } from 'reselector';
import { RootState } from 'somewhere';
export const createAppSelector: TypedCreateSelector<RootState> = createSelector;

// someSelector.ts
import { createAppSelector } from 'utils.ts';
const someSelector = createAppSelector(state => someState, someCombiner); // `state` here will be inferred to `RootState`, no need for being explicit

createAppSelector can be defined once and then used anywhere and it enforces stricter typing.

@markerikson markerikson modified the milestones: 4.1, 5.0 Oct 17, 2021
@markerikson
Copy link
Contributor

I like this idea, but the PR is stale against master, and the new types are complex enough in their own way I'm not sure how to squeeze this in.

@eXamadeus
Copy link
Contributor

eXamadeus commented Oct 27, 2021

So I think this whole PR can be replaced by a simple wrapper type that we provide in either documentation or we add it and export somewhere (might need to workshop the name). It looks like this (95% sure this works)...

  type State = { users: Users, roles: Roles }

  const buildCreateStoreSelector =
    <Store>() =>
    <FirstResult, FinalResult, Selectors extends SelectorArray>(
      first: Selector<Store, FirstResult, never>,
      ...selectors: [
        ...Selectors,
        (
          ...args: SelectorResultArray<[typeof first, ...Selectors]>
        ) => FinalResult
      ]
    ) =>
      createSelector(first, ...selectors)

  const createStoreSelector = buildCreateStoreSelector<State>()

  const selector1 = createStoreSelector(state => state.users, ...)
  const selector2 = createStoreSelector(state => state.roles, ...)

@markerikson I wouldn't mind adding this in as a utility function for this pattern. I've used something similar in a lot of the projects I've been on. On the other hand, this could be a simple tip in the docs somewhere, too.


Personal aside...I've started using this pattern instead:

// utils.ts
export type SimpleSelector<R> = (store: RootStore) => R // RootStore is my application's store type
export const simpleSelector = <R>(selector: SimpleSelector<R>): SimpleSelector<R> => selector
export const useSelector = createSelectorHook<RootStore>()
// auth.selectors.ts
import { createSelector } from '@reduxjs/toolkit'
import { simpleSelector } from './utils'

const selectAuthenticatedSlice = simpleSelector(({ auth: { user } }) =>
  user.state === 'authenticated' ? user : undefined
)

export const authSelectors = {
  user: {
    name: createSelector(selectAuthenticatedSlice, (slice) => slice?.name),
    id: createSelector(selectAuthenticatedSlice, (slice) => slice?.id),
  },
  users: {
    value: simpleSelector(({ auth: { users } }) => (users.state === 'loaded' ? users.value : undefined)),
    state: simpleSelector((store) => store.auth.users.state),
  },
}

then I use it like this...

// UserId.tsx
import { useSelector } from './utils'
import { authSelectors } from './auth.selectors'

const UserId = () => {
  const user = useSelector(authSelectors.user.id)

  return <p>User ID is {user}</p>
}

@markerikson
Copy link
Contributor

I'd say start as a copy-paste pattern, and only add if we need to. New APIs are forever :)

@eXamadeus
Copy link
Contributor

eXamadeus commented Oct 27, 2021

I'd say start as a copy-paste pattern, and only add if we need to. New APIs are forever :)

Yeah, my preference is just making it a copy-paste kinda thing. It's a good bit of overhead to add to the library (tests, making sure it works moving forward, etc).

@gheorghesava-mck
Copy link

Please make sure that the return type is preserved even after applying some equality checks like shallow equal or deep equal. I observed that using useSelector hook with one of the equality checks loses the type of the returned value, being transformed to any

@markerikson
Copy link
Contributor

@gheorghesava-mck that sounds like an entirely different problem. Can you post a separate issue for that, and please include a CodeSandbox or repo that shows the problem happening?

@gheorghesava-mck
Copy link

@markerikson just tested with the latest version and it worked fine. Seems I was behind with some patch versions for react-redux. My apologies!

@siarheipashkevich
Copy link

Any news?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants