diff --git a/index.d.ts b/index.d.ts index 50e8d018fae..d31a15ffea1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -326,7 +326,9 @@ export interface Store { * * @param nextReducer The reducer for the store to use instead. */ - replaceReducer(nextReducer: Reducer): void + replaceReducer( + nextReducer: Reducer + ): Store /** * Interoperability point for observable/reactive libraries. @@ -667,3 +669,9 @@ export function compose( ): (...args: any[]) => R export function compose(...funcs: Function[]): (...args: any[]) => R + +export const __DO_NOT_USE__ActionTypes: { + INIT: string + REPLACE: string + PROBE_UNKNOWN_ACTION: () => string +} diff --git a/src/createStore.js b/src/createStore.js index cef9a2ecaef..1f4a09c5c03 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -238,6 +238,7 @@ export default function createStore(reducer, preloadedState, enhancer) { // will receive the previous state. This effectively populates // the new state tree with any relevant data from the old one. dispatch({ type: ActionTypes.REPLACE }) + return store } /** @@ -284,11 +285,12 @@ export default function createStore(reducer, preloadedState, enhancer) { // the initial state tree. dispatch({ type: ActionTypes.INIT }) - return { + const store = { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } + return store } diff --git a/test/combineReducers.spec.js b/test/combineReducers.spec.ts similarity index 74% rename from test/combineReducers.spec.js rename to test/combineReducers.spec.ts index e0e69aa1109..161e00658b6 100644 --- a/test/combineReducers.spec.js +++ b/test/combineReducers.spec.ts @@ -2,34 +2,40 @@ import { createStore, combineReducers, - __DO_NOT_USE__ActionTypes as ActionTypes -} from '../' + Reducer, + AnyAction, + __DO_NOT_USE__ActionTypes as ActionTypes, + CombinedState +} from '..' describe('Utils', () => { describe('combineReducers', () => { it('returns a composite reducer that maps the state keys to given reducers', () => { const reducer = combineReducers({ - counter: (state = 0, action) => + counter: (state: number = 0, action) => action.type === 'increment' ? state + 1 : state, - stack: (state = [], action) => + stack: (state: any[] = [], action) => action.type === 'push' ? [...state, action.value] : state }) - const s1 = reducer({}, { type: 'increment' }) + const s1 = reducer(undefined, { type: 'increment' }) expect(s1).toEqual({ counter: 1, stack: [] }) const s2 = reducer(s1, { type: 'push', value: 'a' }) expect(s2).toEqual({ counter: 1, stack: ['a'] }) }) it('ignores all props which are not a function', () => { + // we double-cast because these conditions can only happen in a javascript setting const reducer = combineReducers({ - fake: true, - broken: 'string', - another: { nested: 'object' }, + fake: (true as unknown) as Reducer, + broken: ('string' as unknown) as Reducer, + another: ({ nested: 'object' } as unknown) as Reducer, stack: (state = []) => state }) - expect(Object.keys(reducer({}, { type: 'push' }))).toEqual(['stack']) + expect(Object.keys(reducer(undefined, { type: 'push' }))).toEqual([ + 'stack' + ]) }) it('warns if a reducer prop is undefined', () => { @@ -55,7 +61,7 @@ describe('Utils', () => { it('throws an error if a reducer returns undefined handling an action', () => { const reducer = combineReducers({ - counter(state = 0, action) { + counter(state: number = 0, action) { switch (action && action.type) { case 'increment': return state + 1 @@ -77,12 +83,14 @@ describe('Utils', () => { expect(() => reducer({ counter: 0 }, null)).toThrow( /"counter".*an action/ ) - expect(() => reducer({ counter: 0 }, {})).toThrow(/"counter".*an action/) + expect(() => + reducer({ counter: 0 }, ({} as unknown) as AnyAction) + ).toThrow(/"counter".*an action/) }) it('throws an error on first call if a reducer returns undefined initializing', () => { const reducer = combineReducers({ - counter(state, action) { + counter(state: number, action) { switch (action.type) { case 'increment': return state + 1 @@ -93,7 +101,9 @@ describe('Utils', () => { } } }) - expect(() => reducer({})).toThrow(/"counter".*initialization/) + expect(() => reducer(undefined, { type: '' })).toThrow( + /"counter".*initialization/ + ) }) it('catches error thrown in reducer when initializing and re-throw', () => { @@ -102,14 +112,16 @@ describe('Utils', () => { throw new Error('Error thrown in reducer') } }) - expect(() => reducer({})).toThrow(/Error thrown in reducer/) + expect(() => + reducer(undefined, (undefined as unknown) as AnyAction) + ).toThrow(/Error thrown in reducer/) }) it('allows a symbol to be used as an action type', () => { const increment = Symbol('INCREMENT') const reducer = combineReducers({ - counter(state = 0, action) { + counter(state: number = 0, action) { switch (action.type) { case increment: return state + 1 @@ -135,7 +147,7 @@ describe('Utils', () => { } }) - const initialState = reducer(undefined, '@@INIT') + const initialState = reducer(undefined, { type: '@@INIT' }) expect(reducer(initialState, { type: 'FOO' })).toBe(initialState) }) @@ -144,7 +156,7 @@ describe('Utils', () => { child1(state = {}) { return state }, - child2(state = { count: 0 }, action) { + child2(state: { count: number } = { count: 0 }, action) { switch (action.type) { case 'increment': return { count: state.count + 1 } @@ -157,7 +169,7 @@ describe('Utils', () => { } }) - const initialState = reducer(undefined, '@@INIT') + const initialState = reducer(undefined, { type: '@@INIT' }) expect(reducer(initialState, { type: 'increment' })).not.toBe( initialState ) @@ -165,7 +177,7 @@ describe('Utils', () => { it('throws an error on first call if a reducer attempts to handle a private action', () => { const reducer = combineReducers({ - counter(state, action) { + counter(state: number, action) { switch (action.type) { case 'increment': return state + 1 @@ -179,7 +191,9 @@ describe('Utils', () => { } } }) - expect(() => reducer()).toThrow(/"counter".*private/) + expect(() => + reducer(undefined, (undefined as unknown) as AnyAction) + ).toThrow(/"counter".*private/) }) it('warns if no reducers are passed to combineReducers', () => { @@ -188,7 +202,7 @@ describe('Utils', () => { console.error = spy const reducer = combineReducers({}) - reducer({}) + reducer(undefined, { type: '' }) expect(spy.mock.calls[0][0]).toMatch( /Store does not have a valid reducer/ ) @@ -200,9 +214,17 @@ describe('Utils', () => { it('warns if input state does not match reducer shape', () => { const preSpy = console.error const spy = jest.fn() + const nullAction = (undefined as unknown) as AnyAction console.error = spy - const reducer = combineReducers({ + interface ShapeState { + foo: { bar: number } + baz: { qux: number } + } + + type ShapeMismatchState = CombinedState + + const reducer = combineReducers({ foo(state = { bar: 1 }) { return state }, @@ -211,44 +233,51 @@ describe('Utils', () => { } }) - reducer() + reducer(undefined, nullAction) expect(spy.mock.calls.length).toBe(0) - reducer({ foo: { bar: 2 } }) + reducer(({ foo: { bar: 2 } } as unknown) as ShapeState, nullAction) expect(spy.mock.calls.length).toBe(0) - reducer({ - foo: { bar: 2 }, - baz: { qux: 4 } - }) + reducer( + { + foo: { bar: 2 }, + baz: { qux: 4 } + }, + nullAction + ) expect(spy.mock.calls.length).toBe(0) - createStore(reducer, { bar: 2 }) + createStore(reducer, ({ bar: 2 } as unknown) as ShapeState) expect(spy.mock.calls[0][0]).toMatch( /Unexpected key "bar".*createStore.*instead: "foo", "baz"/ ) - createStore(reducer, { bar: 2, qux: 4, thud: 5 }) + createStore(reducer, ({ + bar: 2, + qux: 4, + thud: 5 + } as unknown) as ShapeState) expect(spy.mock.calls[1][0]).toMatch( /Unexpected keys "qux", "thud".*createStore.*instead: "foo", "baz"/ ) - createStore(reducer, 1) + createStore(reducer, (1 as unknown) as ShapeState) expect(spy.mock.calls[2][0]).toMatch( /createStore has unexpected type of "Number".*keys: "foo", "baz"/ ) - reducer({ corge: 2 }) + reducer(({ corge: 2 } as unknown) as ShapeState, nullAction) expect(spy.mock.calls[3][0]).toMatch( /Unexpected key "corge".*reducer.*instead: "foo", "baz"/ ) - reducer({ fred: 2, grault: 4 }) + reducer(({ fred: 2, grault: 4 } as unknown) as ShapeState, nullAction) expect(spy.mock.calls[4][0]).toMatch( /Unexpected keys "fred", "grault".*reducer.*instead: "foo", "baz"/ ) - reducer(1) + reducer((1 as unknown) as ShapeState, nullAction) expect(spy.mock.calls[5][0]).toMatch( /reducer has unexpected type of "Number".*keys: "foo", "baz"/ ) @@ -261,25 +290,32 @@ describe('Utils', () => { const preSpy = console.error const spy = jest.fn() console.error = spy + const nullAction = { type: '' } const foo = (state = { foo: 1 }) => state const bar = (state = { bar: 2 }) => state expect(spy.mock.calls.length).toBe(0) + interface WarnState { + foo: { foo: number } + bar: { bar: number } + } + const reducer = combineReducers({ foo, bar }) - const state = { foo: 1, bar: 2, qux: 3 } + const state = ({ foo: 1, bar: 2, qux: 3 } as unknown) as WarnState + const bazState = ({ ...state, baz: 5 } as unknown) as WarnState - reducer(state, {}) - reducer(state, {}) - reducer(state, {}) - reducer(state, {}) + reducer(state, nullAction) + reducer(state, nullAction) + reducer(state, nullAction) + reducer(state, nullAction) expect(spy.mock.calls.length).toBe(1) - reducer({ ...state, baz: 5 }, {}) - reducer({ ...state, baz: 5 }, {}) - reducer({ ...state, baz: 5 }, {}) - reducer({ ...state, baz: 5 }, {}) + reducer(bazState, nullAction) + reducer({ ...bazState }, nullAction) + reducer({ ...bazState }, nullAction) + reducer({ ...bazState }, nullAction) expect(spy.mock.calls.length).toBe(2) spy.mockClear()