Skip to content

Fix TypeScript tests to pass for new TypeScript definitions #2671

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

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/advanced/Middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,16 @@ The implementation of [`applyMiddleware()`](../api/applyMiddleware.md) that ship

* It only exposes a subset of the [store API](../api/Store.md) to the middleware: [`dispatch(action)`](../api/Store.md#dispatch) and [`getState()`](../api/Store.md#getState).

* It does a bit of trickery to make sure that if you call `store.dispatch(action)` from your middleware instead of `next(action)`, the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware, as we have seen [previously](AsyncActions.md).
* It does a bit of trickery to make sure that if you call `store.dispatch(action)` from your middleware instead of `next(action)`, the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware, as we have seen [previously](AsyncActions.md). There is one caveat when calling `dispatch` during setup, described below.

* To ensure that you may only apply middleware once, it operates on `createStore()` rather than on `store` itself. Instead of `(store, middlewares) => store`, its signature is `(...middlewares) => (createStore) => createStore`.

Because it is cumbersome to apply functions to `createStore()` before using it, `createStore()` accepts an optional last argument to specify such functions.

#### Caveat: Dispatching During Setup

While `applyMiddleware` executes and sets up your middleware, the `store.dispatch` function will point to the vanilla version provided by `createStore`. Dispatching would result in no other middleware being applied. If you are expecting an interaction with another middleware during setup, you will probably be disappointed. Because of this unexpected behavior, `applyMiddleware` will throw an error if you try to dispatch an action before the set up completes. Instead, you should either communicate directly with that other middleware via a common object (for an API-calling middleware, this may be your API client object) or waiting until after the middleware is constructed with a callback.

### The Final Approach

Given this middleware we just wrote:
Expand Down
70 changes: 41 additions & 29 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

/**
* An *action* is a plain object that represents an intention to change the
* state. Actions are the only way to get data into the store. Any data,
Expand All @@ -12,12 +13,13 @@
* Other than `type`, the structure of an action object is really up to you.
* If you're interested, check out Flux Standard Action for recommendations on
* how actions should be constructed.
*
* @template T the type of the action's `type` tag.
*/
export interface Action {
type: any;
export interface Action<T = any> {
type: T;
}


/* reducers */

/**
Expand All @@ -41,15 +43,18 @@ export interface Action {
*
* *Do not put API calls into reducers.*
*
* @template S State object type.
* @template S The type of state consumed and produced by this reducer.
* @template A The type of actions the reducer can potentially respond to.
*/
export type Reducer<S> = <A extends Action>(state: S, action: A) => S;
export type Reducer<S = any, A extends Action = Action> = (state: S | undefined, action: A) => S;

/**
* Object whose values correspond to different reducer functions.
*
* @template A The type of actions the reducers can potentially respond to.
*/
export interface ReducersMapObject {
[key: string]: Reducer<any>;
export type ReducersMapObject<S = any, A extends Action = Action> = {
[K in keyof S]: Reducer<S[K], A>;
}

/**
Expand All @@ -70,7 +75,7 @@ export interface ReducersMapObject {
* @returns A reducer function that invokes every reducer inside the passed
* object, and builds a state object with the same shape.
*/
export function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>;
export function combineReducers<S, A extends Action = Action>(reducers: ReducersMapObject<S, A>): Reducer<S, A>;


/* store */
Expand All @@ -92,9 +97,11 @@ export function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>;
* function to handle async actions in addition to actions. Middleware may
* transform, delay, ignore, or otherwise interpret actions or async actions
* before passing them to the next middleware.
*
* @template D the type of things (actions or otherwise) which may be dispatched.
*/
export interface Dispatch<S> {
<A extends Action>(action: A): A;
export interface Dispatch<D = Action> {
<A extends D>(action: A): A;
}

/**
Expand All @@ -109,9 +116,11 @@ export interface Unsubscribe {
* There should only be a single store in a Redux app, as the composition
* happens on the reducer level.
*
* @template S State object type.
* @template S The type of state held by this store.
* @template A the type of actions which may be dispatched by this store.
* @template N The type of non-actions which may be dispatched by this store.
*/
export interface Store<S> {
export interface Store<S = any, A extends Action = Action, N = never> {
/**
* Dispatches an action. It is the only way to trigger a state change.
*
Expand All @@ -138,7 +147,7 @@ export interface Store<S> {
* Note that, if you use a custom middleware, it may wrap `dispatch()` to
* return something else (for example, a Promise you can await).
*/
dispatch: Dispatch<S>;
dispatch: Dispatch<A | N>;

/**
* Reads the state tree managed by the store.
Expand Down Expand Up @@ -182,7 +191,7 @@ export interface Store<S> {
*
* @param nextReducer The reducer for the store to use instead.
*/
replaceReducer(nextReducer: Reducer<S>): void;
replaceReducer(nextReducer: Reducer<S, A>): void;
}

/**
Expand All @@ -191,11 +200,13 @@ export interface Store<S> {
* `createStore(reducer, preloadedState)` exported from the Redux package, from
* store creators that are returned from the store enhancers.
*
* @template S State object type.
* @template S The type of state to be held by the store.
* @template A The type of actions which may be dispatched.
* @template D The type of all things which may be dispatched.
*/
export interface StoreCreator {
<S>(reducer: Reducer<S>, enhancer?: StoreEnhancer<S>): Store<S>;
<S>(reducer: Reducer<S>, preloadedState: S, enhancer?: StoreEnhancer<S>): Store<S>;
<S, A extends Action, N>(reducer: Reducer<S, A>, enhancer?: StoreEnhancer<N>): Store<S, A, N>;
<S, A extends Action, N>(reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<N>): Store<S, A, N>;
}

/**
Expand All @@ -215,10 +226,11 @@ export interface StoreCreator {
* provided by the developer tools. It is what makes time travel possible
* without the app being aware it is happening. Amusingly, the Redux
* middleware implementation is itself a store enhancer.
*
*/
export type StoreEnhancer<S> = (next: StoreEnhancerStoreCreator<S>) => StoreEnhancerStoreCreator<S>;
export type GenericStoreEnhancer = <S>(next: StoreEnhancerStoreCreator<S>) => StoreEnhancerStoreCreator<S>;
export type StoreEnhancerStoreCreator<S> = (reducer: Reducer<S>, preloadedState?: S) => Store<S>;
export type StoreEnhancer<N = never> = (next: StoreEnhancerStoreCreator<N>) => StoreEnhancerStoreCreator<N>;
export type GenericStoreEnhancer<N = never> = StoreEnhancer<N>;
export type StoreEnhancerStoreCreator<N = never> = <S = any, A extends Action = Action>(reducer: Reducer<S, A>, preloadedState?: S) => Store<S, A, N>;

/**
* Creates a Redux store that holds the state tree.
Expand Down Expand Up @@ -253,8 +265,8 @@ export const createStore: StoreCreator;

/* middleware */

export interface MiddlewareAPI<S> {
dispatch: Dispatch<S>;
export interface MiddlewareAPI<S = any, D = Action> {
dispatch: Dispatch<D>;
getState(): S;
}

Expand All @@ -268,7 +280,7 @@ export interface MiddlewareAPI<S> {
* asynchronous API call into a series of synchronous actions.
*/
export interface Middleware {
<S>(api: MiddlewareAPI<S>): (next: Dispatch<S>) => Dispatch<S>;
<S = any, D = Action>(api: MiddlewareAPI<S, D>): (next: Dispatch<D>) => Dispatch<D>;
}

/**
Expand Down Expand Up @@ -317,8 +329,8 @@ export interface ActionCreator<A> {
/**
* Object whose values are action creator functions.
*/
export interface ActionCreatorsMapObject {
[key: string]: ActionCreator<any>;
export interface ActionCreatorsMapObject<A = any> {
[key: string]: ActionCreator<A>;
}

/**
Expand All @@ -340,18 +352,18 @@ export interface ActionCreatorsMapObject {
* creator wrapped into the `dispatch` call. If you passed a function as
* `actionCreator`, the return value will also be a single function.
*/
export function bindActionCreators<A extends ActionCreator<any>>(actionCreator: A, dispatch: Dispatch<any>): A;
export function bindActionCreators<A, C extends ActionCreator<A>>(actionCreator: C, dispatch: Dispatch<A>): C;

export function bindActionCreators<
A extends ActionCreator<any>,
B extends ActionCreator<any>
>(actionCreator: A, dispatch: Dispatch<any>): B;

export function bindActionCreators<M extends ActionCreatorsMapObject>(actionCreators: M, dispatch: Dispatch<any>): M;
export function bindActionCreators<A, M extends ActionCreatorsMapObject<A>>(actionCreators: M, dispatch: Dispatch<A>): M;

export function bindActionCreators<
M extends ActionCreatorsMapObject,
N extends ActionCreatorsMapObject
M extends ActionCreatorsMapObject<any>,
N extends ActionCreatorsMapObject<any>
>(actionCreators: M, dispatch: Dispatch<any>): N;


Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"browser": "dist/redux.js",
"main": "lib/index.js",
"module": "es/index.js",
"jsnext:main": "es/index.js",
"typings": "./index.d.ts",
"files": [
"dist",
Expand All @@ -25,7 +24,7 @@
"build:umd": "cross-env BABEL_ENV=es NODE_ENV=development rollup -c -i src/index.js -o dist/redux.js",
"build:umd:min": "cross-env BABEL_ENV=es NODE_ENV=production rollup -c -i src/index.js -o dist/redux.min.js",
"build": "yarn run build:commonjs && yarn run build:es && yarn run build:umd && yarn run build:umd:min",
"prepublish": "yarn run clean && yarn run lint && yarn test && yarn run build && check-es3-syntax lib/ dist/ --kill --print",
"prepare": "yarn run clean && yarn run lint && yarn test && yarn run build && check-es3-syntax lib/ dist/ --kill --print",
"examples:lint": "eslint examples",
"examples:test": "cross-env CI=true babel-node examples/testAll.js",
"docs:clean": "rimraf _book",
Expand Down Expand Up @@ -111,8 +110,8 @@
"rollup-plugin-replace": "^1.1.1",
"rollup-plugin-uglify": "^1.0.1",
"rxjs": "^5.0.0-beta.6",
"typescript": "^1.8.0",
"typescript-definition-tester": "0.0.4"
"typescript": "^2.4.2",
"typescript-definition-tester": "0.0.5"
},
"npmName": "redux",
"npmFileMap": [
Expand Down
7 changes: 6 additions & 1 deletion src/applyMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ import compose from './compose'
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
let chain = []

const middlewareAPI = {
Expand Down
2 changes: 1 addition & 1 deletion src/combineReducers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionTypes } from './createStore'
import ActionTypes from './utils/actionTypes'
import isPlainObject from 'lodash/isPlainObject'
import warning from './utils/warning'

Expand Down
34 changes: 25 additions & 9 deletions src/createStore.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import isPlainObject from 'lodash/isPlainObject'
import $$observable from 'symbol-observable'

/**
* These are private action types reserved by Redux.
* For any unknown actions, you must return the current state.
* If the current state is undefined, you must return the initial state.
* Do not reference these action types directly in your code.
*/
export const ActionTypes = {
INIT: '@@redux/INIT'
}
import ActionTypes from './utils/actionTypes'

/**
* Creates a Redux store that holds the state tree.
Expand Down Expand Up @@ -72,6 +64,14 @@ export default function createStore(reducer, preloadedState, enhancer) {
* @returns {any} The current state tree of your application.
*/
function getState() {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}

return currentState
}

Expand Down Expand Up @@ -103,6 +103,15 @@ export default function createStore(reducer, preloadedState, enhancer) {
throw new Error('Expected listener to be a function.')
}

if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
)
}

let isSubscribed = true

ensureCanMutateNextListeners()
Expand All @@ -113,6 +122,13 @@ export default function createStore(reducer, preloadedState, enhancer) {
return
}

if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
)
}

isSubscribed = false

ensureCanMutateNextListeners()
Expand Down
11 changes: 11 additions & 0 deletions src/utils/actionTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* These are private action types reserved by Redux.
* For any unknown actions, you must return the current state.
* If the current state is undefined, you must return the initial state.
* Do not reference these action types directly in your code.
*/
var ActionTypes = {
INIT: '@@redux/INIT'
}

export default ActionTypes
28 changes: 11 additions & 17 deletions test/applyMiddleware.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import { addTodo, addTodoAsync, addTodoIfEmpty } from './helpers/actionCreators'
import { thunk } from './helpers/middleware'

describe('applyMiddleware', () => {
it('warns when dispatching during middleware setup', () => {
function dispatchingMiddleware(store) {
store.dispatch(addTodo('Dont dispatch in middleware setup'))
return next => action => next(action)
}

expect(() =>
applyMiddleware(dispatchingMiddleware)(createStore)(reducers.todos)
).toThrow()
})

it('wraps dispatch method with middleware once', () => {
function test(spyOnMethods) {
return methods => {
Expand Down Expand Up @@ -92,21 +103,4 @@ describe('applyMiddleware', () => {
})
})

it('keeps unwrapped dispatch available while middleware is initializing', () => {
// This is documenting the existing behavior in Redux 3.x.
// We plan to forbid this in Redux 4.x.

function earlyDispatch({ dispatch }) {
dispatch(addTodo('Hello'))
return () => action => action
}

const store = createStore(reducers.todos, applyMiddleware(earlyDispatch))
expect(store.getState()).toEqual([
{
id: 1,
text: 'Hello'
}
])
})
})
3 changes: 2 additions & 1 deletion test/combineReducers.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-console */
import { combineReducers } from '../src'
import createStore, { ActionTypes } from '../src/createStore'
import createStore from '../src/createStore'
import ActionTypes from '../src/utils/actionTypes'

describe('Utils', () => {
describe('combineReducers', () => {
Expand Down
Loading