diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000..ad6973eb71 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,390 @@ +/** + * 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, + * whether from UI events, network callbacks, or other sources such as + * WebSockets needs to eventually be dispatched as actions. + * + * Actions must have a `type` field that indicates the type of action being + * performed. Types can be defined as constants and imported from another + * module. It’s better to use strings for `type` than Symbols because strings + * are serializable. + * + * 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. + */ +export interface Action { + type: any; +} + + +/* reducers */ + +/** + * A *reducer* (also called a *reducing function*) is a function that accepts + * an accumulation and a value and returns a new accumulation. They are used + * to reduce a collection of values down to a single value + * + * Reducers are not unique to Redux—they are a fundamental concept in + * functional programming. Even most non-functional languages, like + * JavaScript, have a built-in API for reducing. In JavaScript, it's + * `Array.prototype.reduce()`. + * + * In Redux, the accumulated value is the state object, and the values being + * accumulated are actions. Reducers calculate a new state given the previous + * state and an action. They must be *pure functions*—functions that return + * the exact same output for given inputs. They should also be free of + * side-effects. This is what enables exciting features like hot reloading and + * time travel. + * + * Reducers are the most important concept in Redux. + * + * *Do not put API calls into reducers.* + * + * @template S State object type. + */ +export type Reducer = (state: S, action: A) => S; + +/** + * Object whose values correspond to different reducer functions. + */ +export interface ReducersMapObject { + [key: string]: Reducer; +} + +/** + * Turns an object whose values are different reducer functions, into a single + * reducer function. It will call every child reducer, and gather their results + * into a single state object, whose keys correspond to the keys of the passed + * reducer functions. + * + * @template S Combined state object type. + * + * @param reducers An object whose values correspond to different reducer + * functions that need to be combined into one. One handy way to obtain it + * is to use ES6 `import * as reducers` syntax. The reducers may never + * return undefined for any action. Instead, they should return their + * initial state if the state passed to them was undefined, and the current + * state for any unrecognized action. + * + * @returns A reducer function that invokes every reducer inside the passed + * object, and builds a state object with the same shape. + */ +export function combineReducers(reducers: ReducersMapObject): Reducer; + + +/* store */ + +/** + * A *dispatching function* (or simply *dispatch function*) is a function that + * accepts an action or an async action; it then may or may not dispatch one + * or more actions to the store. + * + * We must distinguish between dispatching functions in general and the base + * `dispatch` function provided by the store instance without any middleware. + * + * The base dispatch function *always* synchronously sends an action to the + * store’s reducer, along with the previous state returned by the store, to + * calculate a new state. It expects actions to be plain objects ready to be + * consumed by the reducer. + * + * Middleware wraps the base dispatch function. It allows the dispatch + * 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. + */ +export type Dispatch = (action: any) => any; + +/** + * Function to remove listener added by `Store.subscribe()`. + */ +export interface Unsubscribe { + (): void; +} + +/** + * A store is an object that holds the application’s state tree. + * There should only be a single store in a Redux app, as the composition + * happens on the reducer level. + * + * @template S State object type. + */ +export interface Store { + /** + * Dispatches an action. It is the only way to trigger a state change. + * + * The `reducer` function, used to create the store, will be called with the + * current state tree and the given `action`. Its return value will be + * considered the **next** state of the tree, and the change listeners will + * be notified. + * + * The base implementation only supports plain object actions. If you want + * to dispatch a Promise, an Observable, a thunk, or something else, you + * need to wrap your store creating function into the corresponding + * middleware. For example, see the documentation for the `redux-thunk` + * package. Even the middleware will eventually dispatch plain object + * actions using this method. + * + * @param action A plain object representing “what changed”. It is a good + * idea to keep actions serializable so you can record and replay user + * sessions, or use the time travelling `redux-devtools`. An action must + * have a `type` property which may not be `undefined`. It is a good idea + * to use string constants for action types. + * + * @returns For convenience, the same action object you dispatched. + * + * 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; + + /** + * Reads the state tree managed by the store. + * + * @returns The current state tree of your application. + */ + getState(): S; + + /** + * Adds a change listener. It will be called any time an action is + * dispatched, and some part of the state tree may potentially have changed. + * You may then call `getState()` to read the current state tree inside the + * callback. + * + * You may call `dispatch()` from a change listener, with the following + * caveats: + * + * 1. The subscriptions are snapshotted just before every `dispatch()` call. + * If you subscribe or unsubscribe while the listeners are being invoked, + * this will not have any effect on the `dispatch()` that is currently in + * progress. However, the next `dispatch()` call, whether nested or not, + * will use a more recent snapshot of the subscription list. + * + * 2. The listener should not expect to see all states changes, as the state + * might have been updated multiple times during a nested `dispatch()` before + * the listener is called. It is, however, guaranteed that all subscribers + * registered before the `dispatch()` started will be called with the latest + * state by the time it exits. + * + * @param listener A callback to be invoked on every dispatch. + * @returns A function to remove this change listener. + */ + subscribe(listener: () => void): Unsubscribe; + + /** + * Replaces the reducer currently used by the store to calculate the state. + * + * You might need this if your app implements code splitting and you want to + * load some of the reducers dynamically. You might also need this if you + * implement a hot reloading mechanism for Redux. + * + * @param nextReducer The reducer for the store to use instead. + */ + replaceReducer(nextReducer: Reducer): void; +} + +/** + * A store creator is a function that creates a Redux store. Like with + * dispatching function, we must distinguish the base store creator, + * `createStore(reducer, initialState)` exported from the Redux package, from + * store creators that are returned from the store enhancers. + * + * @template S State object type. + */ +export interface StoreCreator { + (reducer: Reducer, enhancer?: StoreEnhancer): Store; + (reducer: Reducer, initialState: S, + enhancer?: StoreEnhancer): Store; +} + +/** + * A store enhancer is a higher-order function that composes a store creator + * to return a new, enhanced store creator. This is similar to middleware in + * that it allows you to alter the store interface in a composable way. + * + * Store enhancers are much the same concept as higher-order components in + * React, which are also occasionally called “component enhancers”. + * + * Because a store is not an instance, but rather a plain-object collection of + * functions, copies can be easily created and modified without mutating the + * original store. There is an example in `compose` documentation + * demonstrating that. + * + * Most likely you’ll never write a store enhancer, but you may use the one + * 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 = (next: StoreCreator) => StoreCreator; + +/** + * Creates a Redux store that holds the state tree. + * The only way to change the data in the store is to call `dispatch()` on it. + * + * There should only be a single store in your app. To specify how different + * parts of the state tree respond to actions, you may combine several + * reducers + * into a single reducer function by using `combineReducers`. + * + * @template S State object type. + * + * @param reducer A function that returns the next state tree, given the + * current state tree and the action to handle. + * + * @param [initialState] The initial state. You may optionally specify it to + * hydrate the state from the server in universal apps, or to restore a + * previously serialized user session. If you use `combineReducers` to + * produce the root reducer function, this must be an object with the same + * shape as `combineReducers` keys. + * + * @param [enhancer] The store enhancer. You may optionally specify it to + * enhance the store with third-party capabilities such as middleware, time + * travel, persistence, etc. The only store enhancer that ships with Redux + * is `applyMiddleware()`. + * + * @returns A Redux store that lets you read the state, dispatch actions and + * subscribe to changes. + */ +export const createStore: StoreCreator; + + +/* middleware */ + +export interface MiddlewareAPI { + dispatch: Dispatch; + getState(): S; +} + +/** + * A middleware is a higher-order function that composes a dispatch function + * to return a new dispatch function. It often turns async actions into + * actions. + * + * Middleware is composable using function composition. It is useful for + * logging actions, performing side effects like routing, or turning an + * asynchronous API call into a series of synchronous actions. + */ +export interface Middleware { + (api: MiddlewareAPI): (next: Dispatch) => (action: any) => any; +} + +/** + * Creates a store enhancer that applies middleware to the dispatch method + * of the Redux store. This is handy for a variety of tasks, such as + * expressing asynchronous actions in a concise manner, or logging every + * action payload. + * + * See `redux-thunk` package as an example of the Redux middleware. + * + * Because middleware is potentially asynchronous, this should be the first + * store enhancer in the composition chain. + * + * Note that each middleware will be given the `dispatch` and `getState` + * functions as named arguments. + * + * @param middlewares The middleware chain to be applied. + * @returns A store enhancer applying the middleware. + */ +export function applyMiddleware(...middlewares: Middleware[]): StoreEnhancer; + + +/* action creators */ + +/** + * An *action creator* is, quite simply, a function that creates an action. Do + * not confuse the two terms—again, an action is a payload of information, and + * an action creator is a factory that creates an action. + * + * Calling an action creator only produces an action, but does not dispatch + * it. You need to call the store’s `dispatch` function to actually cause the + * mutation. Sometimes we say *bound action creators* to mean functions that + * call an action creator and immediately dispatch its result to a specific + * store instance. + * + * If an action creator needs to read the current state, perform an API call, + * or cause a side effect, like a routing transition, it should return an + * async action instead of an action. + * + * @template A Returned action type. + */ +export interface ActionCreator { + (...args: any[]): A; +} + +/** + * Object whose values are action creator functions. + */ +export interface ActionCreatorsMapObject { + [key: string]: ActionCreator; +} + +/** + * Turns an object whose values are action creators, into an object with the + * same keys, but with every function wrapped into a `dispatch` call so they + * may be invoked directly. This is just a convenience method, as you can call + * `store.dispatch(MyActionCreators.doSomething())` yourself just fine. + * + * For convenience, you can also pass a single function as the first argument, + * and get a function in return. + * + * @param actionCreator An object whose values are action creator functions. + * One handy way to obtain it is to use ES6 `import * as` syntax. You may + * also pass a single function. + * + * @param dispatch The `dispatch` function available on your Redux store. + * + * @returns The object mimicking the original object, but with every action + * 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>(actionCreator: A, dispatch: Dispatch): A; + +export function bindActionCreators< + A extends ActionCreator, + B extends ActionCreator + >(actionCreator: A, dispatch: Dispatch): B; + +export function bindActionCreators(actionCreators: M, dispatch: Dispatch): M; + +export function bindActionCreators< + M extends ActionCreatorsMapObject, + N extends ActionCreatorsMapObject + >(actionCreators: M, dispatch: Dispatch): N; + + +/* compose */ + +/** + * Composes single-argument functions from right to left. The rightmost + * function can take multiple arguments as it provides the signature for the + * resulting composite function. + * + * @param funcs The functions to compose. + * @returns R function obtained by composing the argument functions from right + * to left. For example, `compose(f, g, h)` is identical to doing + * `(...args) => f(g(h(...args)))`. + */ +export function compose(): (a: R, ...args: any[]) => R; + +export function compose( + f1: (b: A) => R, + f2: (...args: any[]) => A +): (...args: any[]) => R; + +export function compose( + f1: (b: B) => R, + f2: (a: A) => B, + f3: (...args: any[]) => A +): (...args: any[]) => R; + +export function compose( + f1: (b: C) => R, + f2: (a: B) => C, + f3: (a: A) => B, + f4: (...args: any[]) => A +): (...args: any[]) => R; + +export function compose( + f1: (a: any) => R, + ...funcs: Function[] +): (...args: any[]) => R; diff --git a/package.json b/package.json index 0961676f4d..53915898a8 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Predictable state container for JavaScript apps", "main": "lib/index.js", "jsnext:main": "es/index.js", + "typings": "./index.d.ts", "files": [ "dist", "lib", @@ -99,6 +100,8 @@ "isparta": "^4.0.0", "mocha": "^2.2.5", "rimraf": "^2.3.4", + "typescript": "^1.8.0", + "typescript-definition-tester": "0.0.4", "webpack": "^1.9.6" }, "npmName": "redux", diff --git a/test/typescript.spec.js b/test/typescript.spec.js new file mode 100644 index 0000000000..5d0bc28f0c --- /dev/null +++ b/test/typescript.spec.js @@ -0,0 +1,14 @@ +import * as tt from 'typescript-definition-tester' + + +describe('TypeScript definitions', function () { + this.timeout(0) + + it('should compile against index.d.ts', (done) => { + tt.compileDirectory( + __dirname + '/typescript', + fileName => fileName.match(/\.ts$/), + () => done() + ) + }) +}) diff --git a/test/typescript/actionCreators.ts b/test/typescript/actionCreators.ts new file mode 100644 index 0000000000..77daa9d739 --- /dev/null +++ b/test/typescript/actionCreators.ts @@ -0,0 +1,61 @@ +import { + ActionCreator, Action, Dispatch, + bindActionCreators, ActionCreatorsMapObject +} from "../../index.d.ts"; + + +interface AddTodoAction extends Action { + text: string; +} + +const addTodo: ActionCreator = (text: string) => ({ + type: 'ADD_TODO', + text +}) + +const addTodoAction: AddTodoAction = addTodo('test'); + +type AddTodoThunk = (dispatch: Dispatch) => AddTodoAction; + +const addTodoViaThunk: ActionCreator = (text: string) => + (dispatch: Dispatch) => ({ + type: 'ADD_TODO', + text + }) + +declare const dispatch: Dispatch; + +const boundAddTodo = bindActionCreators(addTodo, dispatch); + +const dispatchedAddTodoAction: AddTodoAction = boundAddTodo('test'); + + +const boundAddTodoViaThunk = bindActionCreators< + ActionCreator, + ActionCreator +>(addTodoViaThunk, dispatch) + +const dispatchedAddTodoViaThunkAction: AddTodoAction = + boundAddTodoViaThunk('test'); + + +const boundActionCreators = bindActionCreators({addTodo}, dispatch); + +const otherDispatchedAddTodoAction: AddTodoAction = + boundActionCreators.addTodo('test'); + + +interface M extends ActionCreatorsMapObject { + addTodoViaThunk: ActionCreator +} + +interface N extends ActionCreatorsMapObject { + addTodoViaThunk: ActionCreator +} + +const boundActionCreators2 = bindActionCreators({ + addTodoViaThunk +}, dispatch) + +const otherDispatchedAddTodoAction2: AddTodoAction = + boundActionCreators2.addTodoViaThunk('test'); diff --git a/test/typescript/actions.ts b/test/typescript/actions.ts new file mode 100644 index 0000000000..1a0bb29d03 --- /dev/null +++ b/test/typescript/actions.ts @@ -0,0 +1,61 @@ +import {Action as ReduxAction} from "../../index.d.ts"; + + +namespace FSA { + interface Action

extends ReduxAction { + payload: P; + } + + const action: Action = { + type: 'ACTION_TYPE', + payload: 'test', + } + + const payload: string = action.payload; +} + + +namespace FreeShapeAction { + interface Action extends ReduxAction { + [key: string]: any; + } + + const action: Action = { + type: 'ACTION_TYPE', + text: 'test', + } + + const text: string = action['text']; +} + + +namespace StringLiteralTypeAction { + type ActionType = 'A' | 'B' | 'C'; + + interface Action extends ReduxAction { + type: ActionType; + } + + const action: Action = { + type: 'A' + } + + const type: ActionType = action.type; +} + + +namespace EnumTypeAction { + enum ActionType { + A, B, C + } + + interface Action extends ReduxAction { + type: ActionType; + } + + const action: Action = { + type: ActionType.A + } + + const type: ActionType = action.type; +} diff --git a/test/typescript/compose.ts b/test/typescript/compose.ts new file mode 100644 index 0000000000..5ee9328eb9 --- /dev/null +++ b/test/typescript/compose.ts @@ -0,0 +1,22 @@ +import {compose} from "../../index.d.ts"; + +// copied from DefinitelyTyped/compose-function + +const numberToNumber = (a: number): number => a + 2; +const numberToString = (a: number): string => "foo"; +const stringToNumber = (a: string): number => 5; + +const t1: number = compose(numberToNumber, numberToNumber)(5); +const t2: string = compose(numberToString, numberToNumber)(5); +const t3: string = compose(numberToString, stringToNumber)("f"); +const t4: (a: string) => number = compose( + (f: (a: string) => number) => ((p: string) => 5), + (f: (a: number) => string) => ((p: string) => 4) +)(numberToString); + + +const t5: number = compose(stringToNumber, numberToString, numberToNumber)(5); +const t6: string = compose(numberToString, stringToNumber, numberToString, numberToNumber)(5); + +const t7: string = compose( + numberToString, numberToNumber, stringToNumber, numberToString, stringToNumber)("fo"); diff --git a/test/typescript/dispatch.ts b/test/typescript/dispatch.ts new file mode 100644 index 0000000000..b589592f1b --- /dev/null +++ b/test/typescript/dispatch.ts @@ -0,0 +1,12 @@ +import {Dispatch, Action} from "../../index.d.ts"; + + +declare const dispatch: Dispatch; + + +const dispatchResult: Action = dispatch({type: 'TYPE'}); + + +type Thunk = () => O; + +const dispatchThunkResult: number = dispatch(() => 42); diff --git a/test/typescript/middleware.ts b/test/typescript/middleware.ts new file mode 100644 index 0000000000..d5738dc844 --- /dev/null +++ b/test/typescript/middleware.ts @@ -0,0 +1,60 @@ +import { + Middleware, MiddlewareAPI, + applyMiddleware, createStore, Dispatch, Reducer, Action +} from "../../index.d.ts"; + + +type Thunk = (dispatch: Dispatch, getState?: () => S) => O; + +const thunkMiddleware: Middleware = + ({dispatch, getState}: MiddlewareAPI) => + (next: Dispatch) => + (action: A | Thunk): B => + typeof action === 'function' ? + (>action)(dispatch, getState) : + next(action) + + +const loggerMiddleware: Middleware = + ({getState}: MiddlewareAPI) => + (next: Dispatch) => + (action: any): any => { + console.log('will dispatch', action) + + // Call the next dispatch method in the middleware chain. + const returnValue = next(action) + + console.log('state after dispatch', getState()) + + // This will likely be the action itself, unless + // a middleware further in chain changed it. + return returnValue + } + + + +type State = { + todos: string[]; +} + +const reducer: Reducer = (state: State, action: Action): State => { + return state; +} + +const storeWithThunkMiddleware = createStore( + reducer, + applyMiddleware(thunkMiddleware) +); + +storeWithThunkMiddleware.dispatch( + (dispatch: Dispatch, getState: () => State) => { + const todos: string[] = getState().todos; + dispatch({type: 'ADD_TODO'}) + } +) + + +const storeWithMultipleMiddleware = createStore( + reducer, + applyMiddleware(loggerMiddleware, thunkMiddleware) +) diff --git a/test/typescript/reducers.ts b/test/typescript/reducers.ts new file mode 100644 index 0000000000..215b872349 --- /dev/null +++ b/test/typescript/reducers.ts @@ -0,0 +1,59 @@ +import { + Reducer, Action, combineReducers, + ReducersMapObject +} from "../../index.d.ts"; + + +type TodosState = string[]; + +interface AddTodoAction extends Action { + text: string; +} + + +const todosReducer: Reducer = (state: TodosState, + action: Action): TodosState => { + switch (action.type) { + case 'ADD_TODO': + return [...state, (action).text] + default: + return state + } +} + +const todosState: TodosState = todosReducer([], { + type: 'ADD_TODO', + text: 'test', +}); + + +type CounterState = number; + + +const counterReducer: Reducer = ( + state: CounterState, action: Action +): CounterState => { + switch (action.type) { + case 'INCREMENT': + return state + 1 + default: + return state + } +} + + +type RootState = { + todos: TodosState; + counter: CounterState; +} + + +const rootReducer: Reducer = combineReducers({ + todos: todosReducer, + counter: counterReducer, +}) + +const rootState: RootState = rootReducer(undefined, { + type: 'ADD_TODO', + text: 'test', +}) diff --git a/test/typescript/store.ts b/test/typescript/store.ts new file mode 100644 index 0000000000..bfb758671b --- /dev/null +++ b/test/typescript/store.ts @@ -0,0 +1,61 @@ +import { + Store, createStore, Reducer, Action, StoreEnhancer, + StoreCreator, Unsubscribe +} from "../../index.d.ts"; + + +type State = { + todos: string[]; +} + +const reducer: Reducer = (state: State, action: Action): State => { + return state; +} + + +/* createStore */ + +const store: Store = createStore(reducer); + +const storeWithInitialState: Store = createStore(reducer, { + todos: [] +}); + +const enhancer: StoreEnhancer = (next: StoreCreator) => next; + +const storeWithEnhancer: Store = createStore(reducer, enhancer); + +const storeWithInitialStateAndEnhancer: Store = createStore(reducer, { + todos: [] +}, enhancer); + + +/* dispatch */ + +store.dispatch({ + type: 'ADD_TODO', + text: 'test' +}) + + +/* getState */ + +const state: State = store.getState(); + + +/* subscribe / unsubscribe */ + +const unsubscribe: Unsubscribe = store.subscribe(() => { + console.log('Current state:', store.getState()) +}) + +unsubscribe(); + + +/* replaceReducer */ + +const newReducer: Reducer = (state: State, action: Action): State => { + return state; +} + +store.replaceReducer(newReducer);