From 5a084a62fda9ebf39ffa969458d9950b4f051688 Mon Sep 17 00:00:00 2001 From: Nathan Bierema <nbierema@gmail.com> Date: Sun, 12 Feb 2023 14:47:15 -0500 Subject: [PATCH] Fix type of next parameter in StoreEnhancer type --- src/applyMiddleware.ts | 9 +--- src/createStore.ts | 33 +++++++++---- src/index.ts | 3 +- src/types/store.ts | 43 ++++++----------- test/typescript/enhancers.ts | 92 +++++++++++++++++++++++++++++------- test/typescript/store.ts | 43 +---------------- 6 files changed, 118 insertions(+), 105 deletions(-) diff --git a/src/applyMiddleware.ts b/src/applyMiddleware.ts index af68589ab0..744ed27895 100644 --- a/src/applyMiddleware.ts +++ b/src/applyMiddleware.ts @@ -1,12 +1,7 @@ import compose from './compose' import { Middleware, MiddlewareAPI } from './types/middleware' import { AnyAction } from './types/actions' -import { - StoreEnhancer, - Dispatch, - PreloadedState, - StoreEnhancerStoreCreator -} from './types/store' +import { StoreEnhancer, Dispatch, PreloadedState } from './types/store' import { Reducer } from './types/reducers' /** @@ -60,7 +55,7 @@ export default function applyMiddleware<Ext, S = any>( export default function applyMiddleware( ...middlewares: Middleware[] ): StoreEnhancer<any> { - return (createStore: StoreEnhancerStoreCreator) => + return createStore => <S, A extends AnyAction>( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S> diff --git a/src/createStore.ts b/src/createStore.ts index 9e13aa2291..921b723c5f 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -39,7 +39,12 @@ import { kindOf } from './utils/kindOf' * `import { legacy_createStore as createStore} from 'redux'` * */ -export function createStore<S, A extends Action, Ext = {}, StateExt = never>( +export function createStore< + S, + A extends Action, + Ext extends {} = {}, + StateExt extends {} = {} +>( reducer: Reducer<S, A>, enhancer?: StoreEnhancer<Ext, StateExt> ): Store<S, A, StateExt> & Ext @@ -68,12 +73,22 @@ export function createStore<S, A extends Action, Ext = {}, StateExt = never>( * `import { legacy_createStore as createStore} from 'redux'` * */ -export function createStore<S, A extends Action, Ext = {}, StateExt = never>( +export function createStore< + S, + A extends Action, + Ext extends {} = {}, + StateExt extends {} = {} +>( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S>, enhancer?: StoreEnhancer<Ext, StateExt> ): Store<S, A, StateExt> & Ext -export function createStore<S, A extends Action, Ext = {}, StateExt = never>( +export function createStore< + S, + A extends Action, + Ext extends {} = {}, + StateExt extends {} = {} +>( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>, enhancer?: StoreEnhancer<Ext, StateExt> @@ -401,8 +416,8 @@ export function createStore<S, A extends Action, Ext = {}, StateExt = never>( export function legacy_createStore< S, A extends Action, - Ext = {}, - StateExt = never + Ext extends {} = {}, + StateExt extends {} = {} >( reducer: Reducer<S, A>, enhancer?: StoreEnhancer<Ext, StateExt> @@ -440,8 +455,8 @@ export function legacy_createStore< export function legacy_createStore< S, A extends Action, - Ext = {}, - StateExt = never + Ext extends {} = {}, + StateExt extends {} = {} >( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S>, @@ -450,8 +465,8 @@ export function legacy_createStore< export function legacy_createStore< S, A extends Action, - Ext = {}, - StateExt = never + Ext extends {} = {}, + StateExt extends {} = {} >( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>, diff --git a/src/index.ts b/src/index.ts index 926be4d143..0fa083343b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,8 +18,7 @@ export { Store, StoreCreator, StoreEnhancer, - StoreEnhancerStoreCreator, - ExtendState + StoreEnhancerStoreCreator } from './types/store' // reducers export { diff --git a/src/types/store.ts b/src/types/store.ts index fb5b1a1d5f..c477cb6471 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -2,20 +2,6 @@ import { Action, AnyAction } from './actions' import { Reducer } from './reducers' import '../utils/symbol-observable' -/** - * Extend the state - * - * This is used by store enhancers and store creators to extend state. - * If there is no state extension, it just returns the state, as is, otherwise - * it returns the state joined with its extension. - * - * Reference for future devs: - * https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919 - */ -export type ExtendState<State, Extension> = [Extension] extends [never] - ? State - : State & Extension - /** * Internal "virtual" symbol used to make the `CombinedState` type unique. */ @@ -134,11 +120,7 @@ export type Observer<T> = { * @template A the type of actions which may be dispatched by this store. * @template StateExt any extension to state from store enhancers */ -export interface Store< - S = any, - A extends Action = AnyAction, - StateExt = never -> { +export interface Store<S = any, A extends Action = AnyAction, StateExt = {}> { /** * Dispatches an action. It is the only way to trigger a state change. * @@ -172,7 +154,7 @@ export interface Store< * * @returns The current state tree of your application. */ - getState(): ExtendState<S, StateExt> + getState(): S & StateExt /** * Adds a change listener. It will be called any time an action is @@ -217,7 +199,7 @@ export interface Store< * For more information, see the observable proposal: * https://github.com/tc39/proposal-observable */ - [Symbol.observable](): Observable<ExtendState<S, StateExt>> + [Symbol.observable](): Observable<S & StateExt> } /** @@ -232,11 +214,11 @@ export interface Store< * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - <S, A extends Action, Ext = {}, StateExt = never>( + <S, A extends Action, Ext extends {} = {}, StateExt extends {} = {}>( reducer: Reducer<S, A>, enhancer?: StoreEnhancer<Ext, StateExt> ): Store<S, A, StateExt> & Ext - <S, A extends Action, Ext = {}, StateExt = never>( + <S, A extends Action, Ext extends {} = {}, StateExt extends {} = {}>( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S>, enhancer?: StoreEnhancer<Ext> @@ -264,13 +246,16 @@ export interface StoreCreator { * @template Ext Store extension that is mixed into the Store type. * @template StateExt State extension that is mixed into the state type. */ -export type StoreEnhancer<Ext = {}, StateExt = never> = ( - next: StoreEnhancerStoreCreator<Ext, StateExt> -) => StoreEnhancerStoreCreator<Ext, StateExt> -export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = < - S = any, - A extends Action = AnyAction +export type StoreEnhancer<Ext extends {} = {}, StateExt extends {} = {}> = < + NextExt extends {}, + NextStateExt extends {} >( + next: StoreEnhancerStoreCreator<NextExt, NextStateExt> +) => StoreEnhancerStoreCreator<NextExt & Ext, NextStateExt & StateExt> +export type StoreEnhancerStoreCreator< + Ext extends {} = {}, + StateExt extends {} = {} +> = <S = any, A extends Action = AnyAction>( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S> ) => Store<S, A, StateExt> & Ext diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index ece858f652..5acdbb8d0a 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -64,11 +64,13 @@ function stateExtension() { reducer: Reducer<S, A>, preloadedState?: any ) => { - const wrappedReducer: Reducer<S & ExtraState, A> = (state, action) => { - const newState = reducer(state, action) - return { - ...newState, - extraField: 'extra' + function wrapReducer(reducer: Reducer<S, A>): Reducer<S & ExtraState, A> { + return (state, action) => { + const newState = reducer(state, action) + return { + ...newState, + extraField: 'extra' + } } } const wrappedPreloadedState = preloadedState @@ -77,7 +79,13 @@ function stateExtension() { extraField: 'extra' } : undefined - return createStore(wrappedReducer, wrappedPreloadedState) + const store = createStore(wrapReducer(reducer), wrappedPreloadedState) + return { + ...store, + replaceReducer(nextReducer: Reducer<S, A>) { + store.replaceReducer(wrapReducer(nextReducer)) + } + } } const store = createStore(reducer, enhancer) @@ -96,8 +104,10 @@ function extraMethods() { createStore => (...args) => { const store = createStore(...args) - store.method = () => 'foo' - return store + return { + ...store, + method: () => 'foo' + } } const store = createStore(reducer, enhancer) @@ -122,11 +132,13 @@ function replaceReducerExtender() { reducer: Reducer<S, A>, preloadedState?: any ) => { - const wrappedReducer: Reducer<S & ExtraState, A> = (state, action) => { - const newState = reducer(state, action) - return { - ...newState, - extraField: 'extra' + function wrapReducer(reducer: Reducer<S, A>): Reducer<S & ExtraState, A> { + return (state, action) => { + const newState = reducer(state, action) + return { + ...newState, + extraField: 'extra' + } } } const wrappedPreloadedState = preloadedState @@ -135,7 +147,14 @@ function replaceReducerExtender() { extraField: 'extra' } : undefined - return createStore(wrappedReducer, wrappedPreloadedState) + const store = createStore(wrapReducer(reducer), wrappedPreloadedState) + return { + ...store, + replaceReducer(nextReducer: Reducer<S, A>) { + store.replaceReducer(wrapReducer(nextReducer)) + }, + method: () => 'foo' + } } const store = createStore< @@ -270,14 +289,14 @@ function finalHelmersonExample() { <S, A extends Action<unknown>>( reducer: Reducer<S, A>, preloadedState?: any - ): Store<S, A, ExtraState> & { persistor: Store<S, A, ExtraState> } => { + ) => { const persistedReducer = persistReducer<S, A>(persistConfig, reducer) const store = createStore(persistedReducer, preloadedState) const persistor = persistStore(store) return { ...store, - replaceReducer: nextReducer => { + replaceReducer: (nextReducer: Reducer<S, A>) => { store.replaceReducer(persistReducer(persistConfig, nextReducer)) }, persistor @@ -308,3 +327,44 @@ function finalHelmersonExample() { // @ts-expect-error store.getState().wrongField } + +function composedEnhancers() { + interface State { + someState: string + } + const reducer: Reducer<State> = null as any + + interface Ext1 { + enhancer1: string + } + interface Ext2 { + enhancer2: number + } + + const enhancer1: StoreEnhancer<Ext1> = + createStore => (reducer, preloadedState) => { + const store = createStore(reducer, preloadedState) + return { + ...store, + enhancer1: 'foo' + } + } + + const enhancer2: StoreEnhancer<Ext2> = + createStore => (reducer, preloadedState) => { + const store = createStore(reducer, preloadedState) + return { + ...store, + enhancer2: 5 + } + } + + const composedEnhancer: StoreEnhancer<Ext1 & Ext2> = createStore => + enhancer2(enhancer1(createStore)) + + const enhancedStore = createStore(reducer, composedEnhancer) + enhancedStore.enhancer1 + enhancedStore.enhancer2 + // @ts-expect-error + enhancedStore.enhancer3 +} diff --git a/test/typescript/store.ts b/test/typescript/store.ts index 3cdb6b769c..5d4158e526 100644 --- a/test/typescript/store.ts +++ b/test/typescript/store.ts @@ -5,8 +5,7 @@ import { Action, StoreEnhancer, Unsubscribe, - Observer, - ExtendState + Observer } from '../../src' import 'symbol-observable' @@ -22,46 +21,6 @@ type State = { e: BrandedString } -/* extended state */ -const noExtend: ExtendState<State, never> = { - a: 'a', - b: { - c: 'c', - d: 'd' - }, - e: brandedString -} - -const noExtendError: ExtendState<State, never> = { - a: 'a', - b: { - c: 'c', - d: 'd' - }, - e: brandedString, - // @ts-expect-error - f: 'oops' -} - -const yesExtend: ExtendState<State, { yes: 'we can' }> = { - a: 'a', - b: { - c: 'c', - d: 'd' - }, - e: brandedString, - yes: 'we can' -} -// @ts-expect-error -const yesExtendError: ExtendState<State, { yes: 'we can' }> = { - a: 'a', - b: { - c: 'c', - d: 'd' - }, - e: brandedString -} - interface DerivedAction extends Action { type: 'a' b: 'b'