-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Using createSelector with EntityAdapter's getSelectors causes '$CombinedState' error #2068
Comments
I believe this may be a |
Hmm. I'm not sure if it is or not. |
That happens because you are building declarations. If you don't need them, you can turn that off in your |
@markerikson @phryneas thanks for the quick reply. @phryneas do you mean
|
I tested pinning |
Any updates on it? I tried to downgrade the peer dependency |
This is a problem with three packages interacting with each other in a specific TypeScript setup - without a minimal reproduction I don't think we can do anything here (and even then it might be tricky) |
Yeah. What we really need is a complete project that actually reproduces the problem and shows this error happening. Without that, there really isn't anything we can do. |
Will try to create a reproducible example project. In mean time, I've identified that the breaking change occurred in Other versions:
|
We definitely iterated on the Reselect TS typings between 4.1.0 and 4.1.5. 4.1.5 should be "correct" behavior. Test against that. |
Just tried - still shows the errors in |
I have also just run into this when reselect was updated from All selectors created with |
We really need a repro at this point :( I'd also like to know if this happens with Reselect by itself as well. (tbh I don't have much time to look at this myself right now. if anyone would like to investigate this, that would really be helpful. and if there's any chance of me actually looking at it, I need a full reproduction that I can clone and immediately see the error happening!) |
Here you go. Just run: npm i
npm run build While setting this up, I'm 99% sure it's the use of |
Oh, yeah, you should never actually use That type was added to the community types years ago, and tbh I don't even know why it's there. It was probably added before This is part of why we specifically show actually inferring the real What happens if you drop that part of it, and switch over to actually following the TS setup approach shown in our docs? |
Hmm. Okay, this does seem to be RTK-related somehow. I tried switching the example over to use import { createSelector } from "@reduxjs/toolkit";
// import { DefaultRootState } from "react-redux";
import type { RootState } from './store';
type AlternateRootState = {
key1: string,
key2: string
}
export const selector = createSelector(
(state: RootState) => state.key1,
(state: RootState) => state.key1,
(key1, key2) => key1 + key2
); but if I switch to
const selector: ((state: {
readonly [$CombinedState]?: undefined;
key1: string;
key2: string;
}) => string) & OutputSelectorFields<(args_0: string, args_1: string) => string & {
clearCache: () => void;
}> & {
clearCache: () => void;
} The actual Redux core TS types have this definition: /**
* Internal "virtual" symbol used to make the `CombinedState` type unique.
*/
declare const $CombinedState: unique symbol
/**
* State base type for reducers created with `combineReducers()`.
*
* This type allows the `createStore()` method to infer which levels of the
* preloaded state can be partial.
*
* Because Typescript is really duck-typed, a type needs to have some
* identifying property to differentiate it from other types with matching
* prototypes for type checking purposes. That's why this type has the
* `$CombinedState` symbol property. Without the property, this type would
* match any object. The symbol doesn't really exist because it's an internal
* (i.e. not exported), and internally we never check its value. Since it's a
* symbol property, it's not expected to be unumerable, and the value is
* typed as always undefined, so its never expected to have a meaningful
* value anyway. It just makes this type distinquishable from plain `{}`.
*/
interface EmptyObject {
readonly [$CombinedState]?: undefined
}
export type CombinedState<S> = EmptyObject & S I'll be honest and say I don't really understand what's going on here. |
Mmm... actually, if this is related to If so, does this but I also have no idea why the latest Reselect types are causing problems with this and declarations either. |
The root of the problem seems to be That said, the upgrade of reselect did introduce a bunch of implicit any errors where before it seemed to infer the types of selectors from previous ones in the chain, but I actually think typing each one properly is somehow more correct. |
Yeah, the Reselect 4.1 types require that you be very explicit and exact about the input selector types, so it can correctly infer the types of the final selector arguments. Sounds like this ultimately comes down to a Redux core types issue in combination with Reselect. We'll have to dig back into this, but it's possible that this type may not even be needed any more. @Methuselah96 , I know you've done some poking around the core types. Any knowledge of |
From what I understand, CombinedState is a phantom type marker that means "this comes from a combineReducer" to be able to use that information in createStore(reducerFunction<State>) // this can either have preloadedState of undefined or State, but not Partial<State>
createStore(combineReducers({ a, b }) // `undefined`, `{a}`, `{b}` or `{a,b}`, but not a partial `a` or `b`
createStore(combineReducers({ a: combineReducers({ c, d }), b}) // `undefined`, `{a}`, `{b}` or `{a,b}`, and also partial `a`, but not partial `b` |
Now that I think about it, that should probably be just stripped from |
I'm back from a few days holiday and diving into this again. I'm a bit unclear why Why should create migrating from: const reducer1 = (state: string = "value1", action: AnyAction) => { ... };
const reducer2 = (state: string = "value2", action: AnyAction) => { ... };
const reducer3 = (state: string = "value3", action: AnyAction) => { ... };
export const rootReducer = combineReducers({
key1: reducer1,
key2: combineReducers({
key1: reducer2,
key2: reducer3,
}),
}); to: const initialState = {
key1: "value1",
key2: {
key1: "value2",
key2: "value3",
},
};
export const rootReducer = (state = initialState, action: AnyAction) => { ... }; change how Obviously this comes from an extremely naive perspective of not understanding how or why we got here, so just tell me it must be this way and I'll drop it. |
That's the thing. I'm not actually convinced this is necessary. Going back in the Redux repo, the The thing is, though: a Redux store's initial state cannot be "deep partial". It's only partial at the top level. Tbh I'm very confused on whether |
For anyone reading this before a real solution is presented, I've taken the import type { CombinedState } from '@reduxjs/toolkit';
type CleanState<T> = T extends CombinedState<infer S> ? { [K in keyof S]: CleanState<S[K]> } : T; and dropped it in my project so I can wrap the export type RootState = CleanState<ReturnType<typeof rootReducer>>; With this, the project builds fine and the definitions for emitted types for the selectors and state are as expected. |
Actually, the above workaround does remove issue with I've discovered that I can work around the issue if I type the selector manually (bypassing the inferred types), but that kind of defeats the purpose of having the complicated types in reselect IMO. I'm going to raise a new ticket in |
@markerikson The reason it's necessary is because the allowed preloaded state is different depending on whether you're using A normal reducer's type is:
The introduction of The real solution would be to add a third generic to the reducer type which defines the initial state:
Then we could get rid of all of these |
reduxjs/redux#4314 is a very rough draft of what it could look like. |
This is sort of what I was hinting at with my comments above about this not being specifically a |
Taking a step back for a bit: is this maybe actually "just" an issue with Reselect itself? It sounds like we've established that this started happening with the Reselect 4.1.x type changes. Does the same sort of error happen if you do something with a |
No, I agree. The new types in reselect 4.1.x seem to be the problem here. Using RootState directly is not an issue elsewhere. |
I’ll just add that the new reselect types are also not an issue unless you emit the declarations for the project. |
I'm just going to take a stab in the dark and say that the CombinedReducer type there could also have some implications with the TS problems we are seeing with |
I haven't got around to opening a issue in the reselect repo yet, but anecdotally, the types appear to create the issue when reselect infers the types to be things like symbols and enums, where they can by used as types and runtime values (I think that's what makes them special). I haven't dug deeper to see if there are other types that trigger it or not, just noticed that all my errors were either |
Which could definitely be a clue, because those are runtime values and not just compile-time types |
Has there been any progress on this issue, or is there a workaround in the mean time? |
I'm intested in this, too! We're currently using the workaround where we use |
Nope. Last couple months have been very busy, and I haven't had time to look at issues like this. |
We've encountered the same Here is what we did to fix that: 1. Create reducers map object const reducers = {
foo: fooSlice.reducer,
bar: barSlice.reducer,
// Register all your reducers as usual.
}; 2. Define a correctly typed root reducer type ReducersMapObject = typeof reducers;
export const rootReducer: Reducer<
StateFromReducersMapObject<ReducersMapObject>,
ActionFromReducersMapObject<ReducersMapObject>
> = combineReducers(reducers); Unlike 3. Create and export root state as usual export type RootState = ReturnType<typeof rootReducer>; Finally, we'll have a state that does not include
|
I've encountered the same issue and solved it with @thibaultboursier solution but with a small change const reducers = {
foo: fooSlice.reducer,
bar: barSlice.reducer,
};
// No need to explicitly set the type here
export const rootReducer = combineReducers(reducers);
export type RootState = StateFromReducersMapObject<typeof reducers> And in case you need to get the dispatch type, here is what I've used: type AppActions = ActionFromReducersMapObject<typeof reducers>;
type AppDispatch = ExtractDispatchExtensions<[ThunkMiddlewareFor<RootState>]> & Dispatch<AppActions>; |
This should be fixed as of https://github.com/reduxjs/redux-toolkit/releases/tag/v2.0.0-alpha.5 , since we've dropped |
Problem
When using the
EntityAdapter
's exported selectors with reselectscreateSelector
a Typescript error is thrown regarding the $CombinedState symbol not being exported.Example:
Results in the following error
Package Versions
Current:
This code was previously working with the following package versions:
Previous:
I'm not sure at which point it broke as I upgraded to the latest and I'm now working backwards.
The text was updated successfully, but these errors were encountered: