combineSlices.d.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import type { UnknownAction, Reducer, StateFromReducersMapObject } from 'redux';
  2. import type { Id, NonUndefined, Tail, UnionToIntersection, WithOptionalProp } from './tsHelpers';
  3. type SliceLike<ReducerPath extends string, State> = {
  4. reducerPath: ReducerPath;
  5. reducer: Reducer<State>;
  6. };
  7. type AnySliceLike = SliceLike<string, any>;
  8. type SliceLikeReducerPath<A extends AnySliceLike> = A extends SliceLike<infer ReducerPath, any> ? ReducerPath : never;
  9. type SliceLikeState<A extends AnySliceLike> = A extends SliceLike<any, infer State> ? State : never;
  10. export type WithSlice<A extends AnySliceLike> = {
  11. [Path in SliceLikeReducerPath<A>]: SliceLikeState<A>;
  12. };
  13. type ReducerMap = Record<string, Reducer>;
  14. type ExistingSliceLike<DeclaredState> = {
  15. [ReducerPath in keyof DeclaredState]: SliceLike<ReducerPath & string, NonUndefined<DeclaredState[ReducerPath]>>;
  16. }[keyof DeclaredState];
  17. export type InjectConfig = {
  18. /**
  19. * Allow replacing reducer with a different reference. Normally, an error will be thrown if a different reducer instance to the one already injected is used.
  20. */
  21. overrideExisting?: boolean;
  22. };
  23. /**
  24. * A reducer that allows for slices/reducers to be injected after initialisation.
  25. */
  26. export interface CombinedSliceReducer<InitialState, DeclaredState = InitialState> extends Reducer<DeclaredState, UnknownAction, Partial<DeclaredState>> {
  27. /**
  28. * Provide a type for slices that will be injected lazily.
  29. *
  30. * One way to do this would be with interface merging:
  31. * ```ts
  32. *
  33. * export interface LazyLoadedSlices {}
  34. *
  35. * export const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<LazyLoadedSlices>();
  36. *
  37. * // elsewhere
  38. *
  39. * declare module './reducer' {
  40. * export interface LazyLoadedSlices extends WithSlice<typeof booleanSlice> {}
  41. * }
  42. *
  43. * const withBoolean = rootReducer.inject(booleanSlice);
  44. *
  45. * // elsewhere again
  46. *
  47. * declare module './reducer' {
  48. * export interface LazyLoadedSlices {
  49. * customName: CustomState
  50. * }
  51. * }
  52. *
  53. * const withCustom = rootReducer.inject({ reducerPath: "customName", reducer: customSlice.reducer })
  54. * ```
  55. */
  56. withLazyLoadedSlices<Lazy = {}>(): CombinedSliceReducer<InitialState, Id<DeclaredState & Partial<Lazy>>>;
  57. /**
  58. * Inject a slice.
  59. *
  60. * Accepts an individual slice, RTKQ API instance, or a "slice-like" { reducerPath, reducer } object.
  61. *
  62. * ```ts
  63. * rootReducer.inject(booleanSlice)
  64. * rootReducer.inject(baseApi)
  65. * rootReducer.inject({ reducerPath: 'boolean' as const, reducer: newReducer }, { overrideExisting: true })
  66. * ```
  67. *
  68. */
  69. inject<Sl extends Id<ExistingSliceLike<DeclaredState>>>(slice: Sl, config?: InjectConfig): CombinedSliceReducer<InitialState, Id<DeclaredState & WithSlice<Sl>>>;
  70. /**
  71. * Inject a slice.
  72. *
  73. * Accepts an individual slice, RTKQ API instance, or a "slice-like" { reducerPath, reducer } object.
  74. *
  75. * ```ts
  76. * rootReducer.inject(booleanSlice)
  77. * rootReducer.inject(baseApi)
  78. * rootReducer.inject({ reducerPath: 'boolean' as const, reducer: newReducer }, { overrideExisting: true })
  79. * ```
  80. *
  81. */
  82. inject<ReducerPath extends string, State>(slice: SliceLike<ReducerPath, State & (ReducerPath extends keyof DeclaredState ? never : State)>, config?: InjectConfig): CombinedSliceReducer<InitialState, Id<DeclaredState & WithSlice<SliceLike<ReducerPath, String>>>>;
  83. /**
  84. * Create a selector that guarantees that the slices injected will have a defined value when selector is run.
  85. *
  86. * ```ts
  87. * const selectBooleanWithoutInjection = (state: RootState) => state.boolean;
  88. * // ^? boolean | undefined
  89. *
  90. * const selectBoolean = rootReducer.inject(booleanSlice).selector((state) => {
  91. * // if action hasn't been dispatched since slice was injected, this would usually be undefined
  92. * // however selector() uses a Proxy around the first parameter to ensure that it evaluates to the initial state instead, if undefined
  93. * return state.boolean;
  94. * // ^? boolean
  95. * })
  96. * ```
  97. *
  98. * If the reducer is nested inside the root state, a selectState callback can be passed to retrieve the reducer's state.
  99. *
  100. * ```ts
  101. *
  102. * export interface LazyLoadedSlices {};
  103. *
  104. * export const innerReducer = combineSlices(stringSlice).withLazyLoadedSlices<LazyLoadedSlices>();
  105. *
  106. * export const rootReducer = combineSlices({ inner: innerReducer });
  107. *
  108. * export type RootState = ReturnType<typeof rootReducer>;
  109. *
  110. * // elsewhere
  111. *
  112. * declare module "./reducer.ts" {
  113. * export interface LazyLoadedSlices extends WithSlice<typeof booleanSlice> {}
  114. * }
  115. *
  116. * const withBool = innerReducer.inject(booleanSlice);
  117. *
  118. * const selectBoolean = withBool.selector(
  119. * (state) => state.boolean,
  120. * (rootState: RootState) => state.inner
  121. * );
  122. * // now expects to be passed RootState instead of innerReducer state
  123. *
  124. * ```
  125. *
  126. * Value passed to selectorFn will be a Proxy - use selector.original(proxy) to get original state value (useful for debugging)
  127. *
  128. * ```ts
  129. * const injectedReducer = rootReducer.inject(booleanSlice);
  130. * const selectBoolean = injectedReducer.selector((state) => {
  131. * console.log(injectedReducer.selector.original(state).boolean) // possibly undefined
  132. * return state.boolean
  133. * })
  134. * ```
  135. */
  136. selector: {
  137. /**
  138. * Create a selector that guarantees that the slices injected will have a defined value when selector is run.
  139. *
  140. * ```ts
  141. * const selectBooleanWithoutInjection = (state: RootState) => state.boolean;
  142. * // ^? boolean | undefined
  143. *
  144. * const selectBoolean = rootReducer.inject(booleanSlice).selector((state) => {
  145. * // if action hasn't been dispatched since slice was injected, this would usually be undefined
  146. * // however selector() uses a Proxy around the first parameter to ensure that it evaluates to the initial state instead, if undefined
  147. * return state.boolean;
  148. * // ^? boolean
  149. * })
  150. * ```
  151. *
  152. * Value passed to selectorFn will be a Proxy - use selector.original(proxy) to get original state value (useful for debugging)
  153. *
  154. * ```ts
  155. * const injectedReducer = rootReducer.inject(booleanSlice);
  156. * const selectBoolean = injectedReducer.selector((state) => {
  157. * console.log(injectedReducer.selector.original(state).boolean) // undefined
  158. * return state.boolean
  159. * })
  160. * ```
  161. */
  162. <Selector extends (state: DeclaredState, ...args: any[]) => unknown>(selectorFn: Selector): (state: WithOptionalProp<Parameters<Selector>[0], Exclude<keyof DeclaredState, keyof InitialState>>, ...args: Tail<Parameters<Selector>>) => ReturnType<Selector>;
  163. /**
  164. * Create a selector that guarantees that the slices injected will have a defined value when selector is run.
  165. *
  166. * ```ts
  167. * const selectBooleanWithoutInjection = (state: RootState) => state.boolean;
  168. * // ^? boolean | undefined
  169. *
  170. * const selectBoolean = rootReducer.inject(booleanSlice).selector((state) => {
  171. * // if action hasn't been dispatched since slice was injected, this would usually be undefined
  172. * // however selector() uses a Proxy around the first parameter to ensure that it evaluates to the initial state instead, if undefined
  173. * return state.boolean;
  174. * // ^? boolean
  175. * })
  176. * ```
  177. *
  178. * If the reducer is nested inside the root state, a selectState callback can be passed to retrieve the reducer's state.
  179. *
  180. * ```ts
  181. *
  182. * interface LazyLoadedSlices {};
  183. *
  184. * const innerReducer = combineSlices(stringSlice).withLazyLoadedSlices<LazyLoadedSlices>();
  185. *
  186. * const rootReducer = combineSlices({ inner: innerReducer });
  187. *
  188. * type RootState = ReturnType<typeof rootReducer>;
  189. *
  190. * // elsewhere
  191. *
  192. * declare module "./reducer.ts" {
  193. * interface LazyLoadedSlices extends WithSlice<typeof booleanSlice> {}
  194. * }
  195. *
  196. * const withBool = innerReducer.inject(booleanSlice);
  197. *
  198. * const selectBoolean = withBool.selector(
  199. * (state) => state.boolean,
  200. * (rootState: RootState) => state.inner
  201. * );
  202. * // now expects to be passed RootState instead of innerReducer state
  203. *
  204. * ```
  205. *
  206. * Value passed to selectorFn will be a Proxy - use selector.original(proxy) to get original state value (useful for debugging)
  207. *
  208. * ```ts
  209. * const injectedReducer = rootReducer.inject(booleanSlice);
  210. * const selectBoolean = injectedReducer.selector((state) => {
  211. * console.log(injectedReducer.selector.original(state).boolean) // possibly undefined
  212. * return state.boolean
  213. * })
  214. * ```
  215. */
  216. <Selector extends (state: DeclaredState, ...args: any[]) => unknown, RootState>(selectorFn: Selector, selectState: (rootState: RootState, ...args: Tail<Parameters<Selector>>) => WithOptionalProp<Parameters<Selector>[0], Exclude<keyof DeclaredState, keyof InitialState>>): (state: RootState, ...args: Tail<Parameters<Selector>>) => ReturnType<Selector>;
  217. /**
  218. * Returns the unproxied state. Useful for debugging.
  219. * @param state state Proxy, that ensures injected reducers have value
  220. * @returns original, unproxied state
  221. * @throws if value passed is not a state Proxy
  222. */
  223. original: (state: DeclaredState) => InitialState & Partial<DeclaredState>;
  224. };
  225. }
  226. type InitialState<Slices extends Array<AnySliceLike | ReducerMap>> = UnionToIntersection<Slices[number] extends infer Slice ? Slice extends AnySliceLike ? WithSlice<Slice> : StateFromReducersMapObject<Slice> : never>;
  227. export declare function combineSlices<Slices extends [
  228. AnySliceLike | ReducerMap,
  229. ...Array<AnySliceLike | ReducerMap>
  230. ]>(...slices: Slices): CombinedSliceReducer<Id<InitialState<Slices>>>;
  231. export {};