diff --git a/index.d.ts b/index.d.ts index 618336002a..f8b7d22dc9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,6 +20,16 @@ export interface Action { type: T; } +/** + * An Action type which accepts any other properties. + * This is mainly for the use of the `Reducer` type. + * This is not part of `Action` itself to prevent users who are extending `Action. + */ +export interface AnyAction extends Action { + // Allows any extra properties to be defined in an action. + [extraProps: string]: any; +} + /* reducers */ /** @@ -46,7 +56,7 @@ export interface Action { * @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 = (state: S | undefined, action: A) => S; +export type Reducer = (state: S | undefined, action: A) => S; /** * Object whose values correspond to different reducer functions. @@ -75,7 +85,8 @@ export type 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(reducers: ReducersMapObject): Reducer; +export function combineReducers(reducers: ReducersMapObject): Reducer; +export function combineReducers(reducers: ReducersMapObject): Reducer; /* store */ @@ -98,10 +109,11 @@ export function combineReducers(reducers: Reducers * 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. + * @template A The type of things (actions or otherwise) which may be + * dispatched. */ -export interface Dispatch { - (action: A): A; +export interface Dispatch { + (action: T): T; } /** @@ -118,9 +130,8 @@ export interface Unsubscribe { * * @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 { +export interface Store { /** * Dispatches an action. It is the only way to trigger a state change. * @@ -147,7 +158,7 @@ export interface Store { * 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; + dispatch: Dispatch; /** * Reads the state tree managed by the store. @@ -204,36 +215,14 @@ export type DeepPartial = { [K in keyof T]?: DeepPartial }; * * @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. + * @template Ext Store extension that is mixed in to the Store type. + * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - (reducer: Reducer, enhancer?: StoreEnhancer): Store; - (reducer: Reducer, preloadedState: DeepPartial, enhancer?: StoreEnhancer): Store; + (reducer: Reducer, enhancer?: StoreEnhancer): Store & Ext; + (reducer: Reducer, preloadedState: DeepPartial, enhancer?: StoreEnhancer): Store & Ext; } -/** - * 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: StoreEnhancerStoreCreator) => StoreEnhancerStoreCreator; -export type GenericStoreEnhancer = StoreEnhancer; -export type StoreEnhancerStoreCreator = (reducer: Reducer, preloadedState?: DeepPartial) => Store; - /** * 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. @@ -264,11 +253,35 @@ export type StoreEnhancerStoreCreator = = (next: StoreEnhancerStoreCreator) => StoreEnhancerStoreCreator; +export type StoreEnhancerStoreCreator = (reducer: Reducer, preloadedState?: DeepPartial) => Store & Ext; + /* middleware */ -export interface MiddlewareAPI { - dispatch: Dispatch; +export interface MiddlewareAPI { + dispatch: D; getState(): S; } @@ -280,9 +293,14 @@ export interface MiddlewareAPI { * 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. + * + * @template DispatchExt Extra Dispatch signature added by this middleware. + * @template S The type of the state supported by this middleware. + * @template D The type of Dispatch of the store where this middleware is + * installed. */ -export interface Middleware { - (api: MiddlewareAPI): (next: Dispatch) => Dispatch; +export interface Middleware { + (api: MiddlewareAPI): (next: Dispatch) => (action: any) => any; } /** @@ -301,8 +319,17 @@ export interface Middleware { * * @param middlewares The middleware chain to be applied. * @returns A store enhancer applying the middleware. + * + * @template Ext Dispatch signature added by a middleware. + * @template S The type of the state supported by a middleware. */ -export function applyMiddleware(...middlewares: Middleware[]): GenericStoreEnhancer; +export function applyMiddleware(): StoreEnhancer; +export function applyMiddleware(middleware1: Middleware): StoreEnhancer<{dispatch: Ext1}>; +export function applyMiddleware(middleware1: Middleware, middleware2: Middleware): StoreEnhancer<{dispatch: Ext1 & Ext2}>; +export function applyMiddleware(middleware1: Middleware, middleware2: Middleware, middleware3: Middleware): StoreEnhancer<{dispatch: Ext1 & Ext2 & Ext3}>; +export function applyMiddleware(middleware1: Middleware, middleware2: Middleware, middleware3: Middleware, middleware4: Middleware): StoreEnhancer<{dispatch: Ext1 & Ext2 & Ext3 & Ext4}>; +export function applyMiddleware(middleware1: Middleware, middleware2: Middleware, middleware3: Middleware, middleware4: Middleware, middleware5: Middleware): StoreEnhancer<{dispatch: Ext1 & Ext2 & Ext3 & Ext4 & Ext5}>; +export function applyMiddleware(...middlewares: Middleware[]): StoreEnhancer<{dispatch: Ext}>; /* action creators */ @@ -354,19 +381,19 @@ 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>(actionCreator: C, dispatch: Dispatch): C; +export function bindActionCreators>(actionCreator: C, dispatch: Dispatch): C; export function bindActionCreators< A extends ActionCreator, B extends ActionCreator - >(actionCreator: A, dispatch: Dispatch): B; + >(actionCreator: A, dispatch: Dispatch): B; -export function bindActionCreators>(actionCreators: M, dispatch: Dispatch): M; +export function bindActionCreators>(actionCreators: M, dispatch: Dispatch): M; export function bindActionCreators< M extends ActionCreatorsMapObject, N extends ActionCreatorsMapObject - >(actionCreators: M, dispatch: Dispatch): N; + >(actionCreators: M, dispatch: Dispatch): N; /* compose */ diff --git a/package-lock.json b/package-lock.json index 847694596d..827cc5d34f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -232,12 +232,6 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, - "assertion-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", - "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", - "dev": true - }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -2046,56 +2040,6 @@ "isarray": "1.0.0" } }, - "dts-bundle": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dts-bundle/-/dts-bundle-0.2.0.tgz", - "integrity": "sha1-4WXklLAPgaO262Q4XL9tG0hrepk=", - "dev": true, - "requires": { - "detect-indent": "0.2.0", - "glob": "4.5.3", - "mkdirp": "0.5.1" - }, - "dependencies": { - "detect-indent": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-0.2.0.tgz", - "integrity": "sha1-BCkUSYl5rC2fPHPk/z5od9O8krY=", - "dev": true, - "requires": { - "get-stdin": "0.1.0", - "minimist": "0.1.0" - } - }, - "glob": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", - "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", - "dev": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "2.0.10", - "once": "1.4.0" - } - }, - "minimatch": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", - "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.1.0.tgz", - "integrity": "sha1-md9lelJXTCHJBXSX33QnkLK0wN4=", - "dev": true - } - } - }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", @@ -3658,6 +3602,15 @@ } } }, + "string_decoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -3669,15 +3622,6 @@ "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", - "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", @@ -3873,12 +3817,6 @@ "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", "dev": true }, - "get-stdin": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-0.1.0.tgz", - "integrity": "sha1-WZivJKr8gC0VyCxoVlfuuLENSpE=", - "dev": true - }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -5734,7 +5672,6 @@ "integrity": "sha512-pt5ClxEmY/dLpb60SmGQQBKi3nB6Ljx1FXmpoCUdAULlGqGVn2uCyXxPCWFbcuHGthT7qGiaGa1wOfs/UjGYMw==", "dev": true, "requires": { - "JSONStream": "1.3.1", "abbrev": "1.1.0", "ansi-regex": "3.0.0", "ansicolors": "0.3.2", @@ -5766,6 +5703,7 @@ "inherits": "2.0.3", "ini": "1.3.4", "init-package-json": "1.10.1", + "JSONStream": "1.3.1", "lazy-property": "1.0.0", "lockfile": "1.0.3", "lodash._baseindexof": "3.1.0", @@ -5832,30 +5770,6 @@ "write-file-atomic": "2.1.0" }, "dependencies": { - "JSONStream": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", - "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", - "dev": true, - "requires": { - "jsonparse": "1.3.1", - "through": "2.3.8" - }, - "dependencies": { - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - } - } - }, "abbrev": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", @@ -6321,6 +6235,30 @@ } } }, + "JSONStream": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", + "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", + "dev": true, + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + }, + "dependencies": { + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + } + } + }, "lazy-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lazy-property/-/lazy-property-1.0.0.tgz", @@ -11320,6 +11258,15 @@ } } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", @@ -11374,15 +11321,6 @@ } } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", @@ -11592,26 +11530,24 @@ "dev": true }, "typescript": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", - "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", + "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", "dev": true }, - "typescript-definition-tester": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/typescript-definition-tester/-/typescript-definition-tester-0.0.5.tgz", - "integrity": "sha1-kcV0146gW4HtgSRNUOww2CQMNW8=", + "typings-tester": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/typings-tester/-/typings-tester-0.3.0.tgz", + "integrity": "sha512-tcQ1k72npmCeQdqjjwMN751ZmwxU69BilTPwGvVpvHTc/g/SZwYYAjIWMtArJxKZl7qgNTCly6mBdjqj0aEDbw==", "dev": true, "requires": { - "assertion-error": "1.0.2", - "dts-bundle": "0.2.0", - "lodash": "3.10.1" + "commander": "2.12.2" }, "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", "dev": true } } diff --git a/package.json b/package.json index a96167ac3a..497a3645f1 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,8 @@ "rollup-plugin-replace": "^2.0.0", "rollup-plugin-uglify": "^2.0.1", "rxjs": "^5.5.0", - "typescript": "^2.4.2", - "typescript-definition-tester": "0.0.5" + "typescript": "^2.6.2", + "typings-tester": "^0.3.0" }, "npmName": "redux", "npmFileMap": [ diff --git a/test/typescript.spec.js b/test/typescript.spec.js index 7d0aa42bc9..e2f25f193e 100644 --- a/test/typescript.spec.js +++ b/test/typescript.spec.js @@ -1,14 +1,7 @@ -import * as tt from 'typescript-definition-tester' +import { checkDirectory } from 'typings-tester' describe('TypeScript definitions', function() { - it('should compile against index.d.ts', done => { - tt.compileDirectory( - __dirname + '/typescript', - fileName => fileName.match(/\.ts$/), - { - strictNullChecks: true - }, - () => done() - ) + it('should compile against index.d.ts', () => { + checkDirectory(__dirname + '/typescript') }) }) diff --git a/test/typescript/actionCreators.ts b/test/typescript/actionCreators.ts index deb53bd3f9..8c889b2b3e 100644 --- a/test/typescript/actionCreators.ts +++ b/test/typescript/actionCreators.ts @@ -1,7 +1,7 @@ import { ActionCreator, Action, Dispatch, bindActionCreators, ActionCreatorsMapObject -} from "../../" +} from "redux" interface AddTodoAction extends Action { @@ -15,15 +15,15 @@ const addTodo: ActionCreator = (text: string) => ({ const addTodoAction: AddTodoAction = addTodo('test'); -type AddTodoThunk = (dispatch: Dispatch) => AddTodoAction; +type AddTodoThunk = (dispatch: Dispatch) => AddTodoAction; const addTodoViaThunk: ActionCreator = (text: string) => - (dispatch: Dispatch) => ({ + (dispatch: Dispatch) => ({ type: 'ADD_TODO', text }) -declare const dispatch: Dispatch; +declare const dispatch: Dispatch; const boundAddTodo = bindActionCreators(addTodo, dispatch); diff --git a/test/typescript/actions.ts b/test/typescript/actions.ts index f26fdc5176..934802ceb8 100644 --- a/test/typescript/actions.ts +++ b/test/typescript/actions.ts @@ -1,4 +1,4 @@ -import {Action as ReduxAction} from "../../" +import {Action as ReduxAction} from "redux" namespace FSA { diff --git a/test/typescript/compose.ts b/test/typescript/compose.ts index d48c9c64ca..8257359816 100644 --- a/test/typescript/compose.ts +++ b/test/typescript/compose.ts @@ -1,6 +1,6 @@ -import {compose} from "../../" +import {compose} from "redux" -// copied from DefinitelyTyped/compose-function +// adapted from DefinitelyTyped/compose-function const numberToNumber = (a: number): number => a + 2; const numberToString = (a: number): string => "foo"; @@ -19,7 +19,8 @@ const t5: number = compose(stringToNumber, numberToString, numberToNumber)(5); const t6: string = compose(numberToString, stringToNumber, numberToString, numberToNumber)(5); -const t7: string = compose( +// rest signature +const t7: string = compose( numberToString, numberToNumber, stringToNumber, numberToString, stringToNumber)("fo"); diff --git a/test/typescript/dispatch.ts b/test/typescript/dispatch.ts index c5361dee36..72f8998856 100644 --- a/test/typescript/dispatch.ts +++ b/test/typescript/dispatch.ts @@ -1,16 +1,46 @@ -import {Dispatch, Action} from "../../" +import { Dispatch, AnyAction } from 'redux' +/** + * Default Dispatch type accepts any object with `type` property. + */ +function simple() { + const dispatch: Dispatch = null as any -declare const dispatch: Dispatch; + const a = dispatch({ type: 'INCREMENT', count: 10 }) -const dispatchResult: Action = dispatch({type: 'TYPE'}); + a.count + // typings:expect-error + a.wrongProp -// thunk -declare module "../../" { - export interface Dispatch { - (asyncAction: (dispatch: Dispatch, getState: () => any) => R): R; - } + // typings:expect-error + dispatch('not-an-action') } -const dispatchThunkResult: number = dispatch(() => 42); -const dispatchedTimerId: number = dispatch(d => setTimeout(() => d({type: 'TYPE'}), 1000)); +/** + * Dispatch accepts type argument that restricts allowed action types. + */ +function discriminated() { + interface IncrementAction { + type: 'INCREMENT' + count?: number + } + + interface DecrementAction { + type: 'DECREMENT' + count?: number + } + + // Union of all actions in the app. + type MyAction = IncrementAction | DecrementAction + + const dispatch: Dispatch = null as any + + dispatch({ type: 'INCREMENT' }) + dispatch({ type: 'DECREMENT', count: 10 }) + // Known actions are strictly checked. + // typings:expect-error + dispatch({ type: 'DECREMENT', count: '' }) + // Unknown actions are rejected. + // typings:expect-error + dispatch({ type: 'SOME_OTHER_TYPE' }) +} diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts new file mode 100644 index 0000000000..4670f29f78 --- /dev/null +++ b/test/typescript/enhancers.ts @@ -0,0 +1,73 @@ +import { + StoreEnhancer, + Action, + AnyAction, + Reducer, + createStore, + DeepPartial +} from 'redux' + +interface State { + someField: 'string' +} +const reducer: Reducer = null as any + +/** + * Store enhancer that extends the type of dispatch. + */ +function dispatchExtension() { + type PromiseDispatch = (promise: Promise) => Promise + + const enhancer: StoreEnhancer<{ dispatch: PromiseDispatch }> = null as any + + const store = createStore(reducer, enhancer) + + store.dispatch({ type: 'INCREMENT' }) + store.dispatch(Promise.resolve({ type: 'INCREMENT' })) + // typings:expect-error + store.dispatch('not-an-action') + // typings:expect-error + store.dispatch(Promise.resolve('not-an-action')) +} + +/** + * Store enhancer that extends the type of the state. + */ +function stateExtension() { + interface ExtraState { + extraField: 'extra' + } + + const enhancer: StoreEnhancer<{}, ExtraState> = createStore => < + S, + A extends Action = AnyAction + >( + reducer: Reducer, + preloadedState?: DeepPartial + ) => { + const wrappedReducer: Reducer = null as any + const wrappedPreloadedState: S & ExtraState = null as any + return createStore(wrappedReducer, wrappedPreloadedState) + } + + const store = createStore(reducer, enhancer) + + store.getState().someField + store.getState().extraField + // typings:expect-error + store.getState().wrongField +} + +/** + * Store enhancer that adds methods to the store. + */ +function extraMethods() { + const enhancer: StoreEnhancer<{ method(): string }> = null as any + + const store = createStore(reducer, enhancer) + + store.getState() + const res: string = store.method() + // typings:expect-error + store.wrongMethod() +} diff --git a/test/typescript/injectedDispatch.ts b/test/typescript/injectedDispatch.ts new file mode 100644 index 0000000000..f115b699fe --- /dev/null +++ b/test/typescript/injectedDispatch.ts @@ -0,0 +1,85 @@ +import { Dispatch, Action } from 'redux' + +interface Component

{ + props: P +} + +interface HOC { +

(wrapped: Component

): Component

+} + +declare function connect( + mapDispatchToProps: (dispatch: D) => T +): HOC + +/** + * Inject default dispatch. + */ +function simple() { + const hoc: HOC<{ onClick(): void }> = connect(dispatch => { + return { + onClick() { + dispatch({ type: 'INCREMENT' }) + // typings:expect-error + dispatch(Promise.resolve({ type: 'INCREMENT' })) + // typings:expect-error + dispatch('not-an-action') + } + } + }) +} + +/** + * Inject dispatch that restricts allowed action types. + */ +function discriminated() { + interface IncrementAction { + type: 'INCREMENT' + count?: number + } + + interface DecrementAction { + type: 'DECREMENT' + count?: number + } + + // Union of all actions in the app. + type MyAction = IncrementAction | DecrementAction + + const hoc: HOC<{ onClick(): void }> = connect( + (dispatch: Dispatch) => { + return { + onClick() { + dispatch({ type: 'INCREMENT' }) + dispatch({ type: 'DECREMENT', count: 10 }) + // typings:expect-error + dispatch({ type: 'DECREMENT', count: '' }) + // typings:expect-error + dispatch({ type: 'SOME_OTHER_TYPE' }) + // typings:expect-error + dispatch('not-an-action') + } + } + } + ) +} + +/** + * Inject extended dispatch. + */ +function promise() { + type PromiseDispatch = (promise: Promise) => Promise + + type MyDispatch = Dispatch & PromiseDispatch + + const hoc: HOC<{ onClick(): void }> = connect((dispatch: MyDispatch) => { + return { + onClick() { + dispatch({ type: 'INCREMENT' }) + dispatch(Promise.resolve({ type: 'INCREMENT' })) + // typings:expect-error + dispatch('not-an-action') + } + } + }) +} diff --git a/test/typescript/middleware.ts b/test/typescript/middleware.ts index 185b7ae3e5..7b1c48443c 100644 --- a/test/typescript/middleware.ts +++ b/test/typescript/middleware.ts @@ -1,64 +1,212 @@ import { - Middleware, MiddlewareAPI, - applyMiddleware, createStore, Dispatch, Reducer, Action -} from "../../" + Middleware, + MiddlewareAPI, + applyMiddleware, + StoreEnhancer, + createStore, + Dispatch, + Reducer, + Action, + AnyAction +} from 'redux' -declare module "../../" { - export interface Dispatch { - (asyncAction: (dispatch: Dispatch, getState: () => any) => R): R; - } +/** + * Logger middleware doesn't add any extra types to dispatch, just logs actions + * and state. + */ +function logger() { + const loggerMiddleware: Middleware = ({ getState }: MiddlewareAPI) => ( + next: Dispatch + ) => action => { + 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 + } + + return loggerMiddleware } -type Thunk = (dispatch: Dispatch, getState?: () => S) => O; +/** + * Promise middleware adds support for dispatching promises. + */ -const thunkMiddleware: Middleware = - ({dispatch, getState}: MiddlewareAPI) => - (next: Dispatch) => - (action: A | Thunk): B|Action => - typeof action === 'function' ? - (>action)(dispatch, getState) : - next(action) +type PromiseDispatch = (promise: Promise) => Promise +function promise() { + const promiseMiddleware: Middleware = ({ + dispatch + }: MiddlewareAPI) => next => ( + action: AnyAction | Promise + ) => { + if (action instanceof Promise) { + action.then(dispatch) + return action + } -const loggerMiddleware: Middleware = - ({getState}: MiddlewareAPI) => - (next: Dispatch) => - (action: any): any => { - console.log('will dispatch', action) + return next(action) + } + + return promiseMiddleware +} - // Call the next dispatch method in the middleware chain. - const returnValue = next(action) +/** + * Thunk middleware adds support for dispatching thunks. + */ - console.log('state after dispatch', getState()) +interface Thunk { + (dispatch: Dispatch & ThunkDispatch & DispatchExt, getState: () => S): R +} - // This will likely be the action itself, unless - // a middleware further in chain changed it. - return returnValue - } +interface ThunkDispatch { + (thunk: Thunk): R +} +function thunk() { + const thunkMiddleware: Middleware< + ThunkDispatch, + S, + Dispatch & ThunkDispatch + > = api => (next: Dispatch) => (action: AnyAction | Thunk) => + typeof action === 'function' + ? action(api.dispatch, api.getState) + : next(action) -type State = { - todos: string[]; + return thunkMiddleware } -const reducer: Reducer = (state: State, action: Action): State => { - return state; +/** + * Middleware that expects exact state type. + */ +function customState() { + type State = { field: 'string' } + + const customMiddleware: Middleware<{}, State> = api => ( + next: Dispatch + ) => action => { + api.getState().field + // typings:expect-error + api.getState().wrongField + + return next(action) + } + + return customMiddleware } -const storeWithThunkMiddleware = createStore( - reducer, - applyMiddleware(thunkMiddleware) -); +/** + * Middleware that expects custom dispatch. + */ +function customDispatch() { + type MyAction = { type: 'INCREMENT' } | { type: 'DECREMENT' } + + // dispatch that expects action union + type MyDispatch = Dispatch + + const customDispatch: Middleware = ( + api: MiddlewareAPI + ) => next => action => { + api.dispatch({ type: 'INCREMENT' }) + api.dispatch({ type: 'DECREMENT' }) + // typings:expect-error + api.dispatch({ type: 'UNKNOWN' }) + } +} -storeWithThunkMiddleware.dispatch( - (dispatch: Dispatch, getState: () => State) => { - const todos: string[] = getState().todos; - dispatch({type: 'ADD_TODO'}) +/** + * Test the type of store.dispatch after applying different middleware. + */ +function apply() { + interface State { + someField: 'string' } -) + const reducer: Reducer = null as any + + /** + * logger + */ + const storeWithLogger = createStore(reducer, applyMiddleware(logger())) + // can only dispatch actions + storeWithLogger.dispatch({ type: 'INCREMENT' }) + // typings:expect-error + storeWithLogger.dispatch(Promise.resolve({ type: 'INCREMENT' })) + // typings:expect-error + storeWithLogger.dispatch('not-an-action') + + /** + * promise + */ + const storeWithPromise = createStore(reducer, applyMiddleware(promise())) + // can dispatch actions and promises + storeWithPromise.dispatch({ type: 'INCREMENT' }) + storeWithPromise.dispatch(Promise.resolve({ type: 'INCREMENT' })) + // typings:expect-error + storeWithPromise.dispatch('not-an-action') + // typings:expect-error + storeWithPromise.dispatch(Promise.resolve('not-an-action')) + /** + * promise + logger + */ + const storeWithPromiseAndLogger = createStore( + reducer, + applyMiddleware(promise(), logger()) + ) + // can dispatch actions and promises + storeWithPromiseAndLogger.dispatch({ type: 'INCREMENT' }) + storeWithPromiseAndLogger.dispatch(Promise.resolve({ type: 'INCREMENT' })) + // typings:expect-error + storeWithPromiseAndLogger.dispatch('not-an-action') + // typings:expect-error + storeWithPromiseAndLogger.dispatch(Promise.resolve('not-an-action')) -const storeWithMultipleMiddleware = createStore( - reducer, - applyMiddleware(thunkMiddleware, loggerMiddleware) -) + /** + * promise + thunk + */ + const storeWithPromiseAndThunk = createStore( + reducer, + applyMiddleware(promise(), thunk(), logger()) + ) + // can dispatch actions, promises and thunks + storeWithPromiseAndThunk.dispatch({ type: 'INCREMENT' }) + storeWithPromiseAndThunk.dispatch(Promise.resolve({ type: 'INCREMENT' })) + storeWithPromiseAndThunk.dispatch((dispatch, getState) => { + getState().someField + // typings:expect-error + getState().wrongField + + // injected dispatch accepts actions, thunks and promises + dispatch({ type: 'INCREMENT' }) + dispatch(dispatch => dispatch({ type: 'INCREMENT' })) + dispatch(Promise.resolve({ type: 'INCREMENT' })) + // typings:expect-error + dispatch('not-an-action') + }) + // typings:expect-error + storeWithPromiseAndThunk.dispatch('not-an-action') + // typings:expect-error + storeWithPromiseAndThunk.dispatch(Promise.resolve('not-an-action')) + + /** + * Test variadic signature. + */ + const storeWithLotsOfMiddleware = createStore( + reducer, + applyMiddleware( + promise(), + logger(), + logger(), + logger(), + logger(), + logger() + ) + ) + storeWithLotsOfMiddleware.dispatch({ type: 'INCREMENT' }) + storeWithLotsOfMiddleware.dispatch(Promise.resolve({ type: 'INCREMENT' })) +} diff --git a/test/typescript/reducers.ts b/test/typescript/reducers.ts index 0fc3c51367..dc1cf10027 100644 --- a/test/typescript/reducers.ts +++ b/test/typescript/reducers.ts @@ -1,59 +1,197 @@ -import { - Reducer, Action, combineReducers, - ReducersMapObject -} from "../../" +import { Reducer, Action, combineReducers, ReducersMapObject } from 'redux' +/** + * Simple reducer definition with no action shape checks. + * Uses string comparison to determine action type. + * + * `AnyAction` type is used to allow action property access without requiring + * type casting. + */ +function simple() { + type State = number -type TodosState = string[]; + const reducer: Reducer = (state = 0, action) => { + if (action.type === 'INCREMENT') { + const { count = 1 } = action -interface AddTodoAction extends Action { - text: string; -} + return state + count + } + if (action.type === 'DECREMENT') { + const { count = 1 } = action -const todosReducer: Reducer = - (state = [], action) => { - switch (action.type) { - case 'ADD_TODO': - return [...state, action.text] - default: - return state + return state - count } + + return state } -const todosState: TodosState = todosReducer([], { - type: 'ADD_TODO', - text: 'test', -}); + // Reducer function accepts any object with `type` prop as action. + // Any extra props are allowed too. + let s: State = reducer(undefined, { type: 'init' }) + s = reducer(s, { type: 'INCREMENT' }) + s = reducer(s, { type: 'INCREMENT', count: 10 }) + s = reducer(s, { type: 'DECREMENT' }) + s = reducer(s, { type: 'DECREMENT', count: 10 }) + s = reducer(s, { type: 'SOME_OTHER_TYPE', someField: 'value' }) + // State shape is strictly checked. + // typings:expect-error + reducer('string', { type: 'INCREMENT' }) -type CounterState = number; + // Combined reducer also accepts any action. + const combined = combineReducers({ sub: reducer }) + let cs: { sub: State } = combined(undefined, { type: 'init' }) + cs = combined(cs, { type: 'INCREMENT', count: 10 }) -const counterReducer: Reducer = ( - state: CounterState, action: Action -): CounterState => { - switch (action.type) { - case 'INCREMENT': - return state + 1 - default: - return state - } + // Combined reducer's state is strictly checked. + // typings:expect-error + combined({ unknown: '' }, { type: 'INCREMENT' }) } +/** + * Reducer definition using discriminated unions. + * + * See https://basarat.gitbooks.io/typescript/content/docs/types/discriminated-unions.html#redux + */ +function discriminated() { + type State = number + + interface IncrementAction { + type: 'INCREMENT' + count?: number + } + + interface DecrementAction { + type: 'DECREMENT' + count?: number + } + + // Union of all actions in the app. + type MyAction = IncrementAction | DecrementAction + + const reducer: Reducer = (state = 0, action) => { + if (action.type === 'INCREMENT') { + // Action shape is determined by `type` discriminator. + // typings:expect-error + action.wrongField + + const { count = 1 } = action + + return state + count + } + + if (action.type === 'DECREMENT') { + // typings:expect-error + action.wrongField + + const { count = 1 } = action + + return state - count + } + + return state + } + + // Reducer state is initialized by Redux using Init action which is private. + // To initialize manually (e.g. in tests) we have to type cast init action + // or add a custom init action to MyAction union. + let s: State = reducer(undefined, { type: 'init' } as any) + s = reducer(s, { type: 'INCREMENT' }) + s = reducer(s, { type: 'INCREMENT', count: 10 }) + // Known actions are strictly checked. + // typings:expect-error + s = reducer(s, { type: 'DECREMENT', coun: 10 }) + s = reducer(s, { type: 'DECREMENT', count: 10 }) + // Unknown actions are rejected. + // typings:expect-error + s = reducer(s, { type: 'SOME_OTHER_TYPE' }) + // typings:expect-error + s = reducer(s, { type: 'SOME_OTHER_TYPE', someField: 'value' }) + + // Combined reducer accepts any action by default which allows to include + // third-party reducers without the need to add their actions to the union. + const combined = combineReducers({ sub: reducer }) + + let cs: { sub: State } = combined(undefined, { type: 'init' }) + cs = combined(cs, { type: 'SOME_OTHER_TYPE' }) -type RootState = { - todos: TodosState; - counter: CounterState; + // Combined reducer can be made to only accept known actions. + const strictCombined = combineReducers<{ sub: State }, MyAction>({ + sub: reducer + }) + + strictCombined(cs, { type: 'INCREMENT' }) + // typings:expect-error + strictCombined(cs, { type: 'SOME_OTHER_TYPE' }) } +/** + * Reducer definition using type guards. + */ +function typeGuards() { + function isAction(action: Action, type: any): action is A { + return action.type === type + } + + type State = number + + interface IncrementAction { + type: 'INCREMENT' + count?: number + } + + interface DecrementAction { + type: 'DECREMENT' + count?: number + } + + const reducer: Reducer = (state = 0, action) => { + if (isAction(action, 'INCREMENT')) { + // Action shape is determined by the type guard returned from `isAction` + // typings:expect-error + action.wrongField + + const { count = 1 } = action + + return state + count + } + + if (isAction(action, 'DECREMENT')) { + // typings:expect-error + action.wrongField + + const { count = 1 } = action + + return state - count + } + + return state + } -const rootReducer = combineReducers({ - todos: todosReducer, - counter: counterReducer, -}) + let s: State = reducer(undefined, { type: 'init' }) + s = reducer(s, { type: 'INCREMENT' }) + s = reducer(s, { type: 'INCREMENT', count: 10 }) + s = reducer(s, { type: 'DECREMENT' }) + s = reducer(s, { type: 'DECREMENT', count: 10 }) + s = reducer(s, { type: 'SOME_OTHER_TYPE', someField: 'value' }) -const rootState: RootState = rootReducer(undefined, { - type: 'ADD_TODO', - text: 'test', -}) + const combined = combineReducers({ sub: reducer }) + + let cs: { sub: State } = combined(undefined, { type: 'init' }) + cs = combined(cs, { type: 'INCREMENT', count: 10 }) +} + +/** + * Test ReducersMapObject with default type args. + */ +function reducersMapObject() { + const obj: ReducersMapObject = {}; + + for (const key of Object.keys(obj)) { + obj[key](undefined, {type: 'SOME_TYPE'}); + // typings:expect-error + obj[key](undefined, 'not-an-action'); + } +} \ No newline at end of file diff --git a/test/typescript/store.ts b/test/typescript/store.ts index 067323e6ba..7fa642259f 100644 --- a/test/typescript/store.ts +++ b/test/typescript/store.ts @@ -1,7 +1,7 @@ import { - Store, createStore, Reducer, Action, StoreEnhancer, GenericStoreEnhancer, + Store, createStore, Reducer, Action, StoreEnhancer, StoreCreator, StoreEnhancerStoreCreator, Unsubscribe -} from "../../" +} from "redux" type State = { @@ -30,15 +30,13 @@ const storeWithPreloadedState: Store = createStore(reducer, { b: {c: 'c'} }); -const genericEnhancer: GenericStoreEnhancer = (next: StoreEnhancerStoreCreator) => next; -const specificEnhancer: StoreEnhancer = next => next; +const enhancer: StoreEnhancer = next => next; -const storeWithGenericEnhancer: Store = createStore(reducer, genericEnhancer); -const storeWithSpecificEnhancer: Store = createStore(reducer, specificEnhancer); +const storeWithSpecificEnhancer: Store = createStore(reducer, enhancer); const storeWithPreloadedStateAndEnhancer: Store = createStore(reducer, { b: {c: 'c'} -}, genericEnhancer); +}, enhancer); /* dispatch */ @@ -65,8 +63,6 @@ unsubscribe(); /* replaceReducer */ -const newReducer: Reducer = (state: State, action: Action): State => { - return state; -} +const newReducer: Reducer = reducer; store.replaceReducer(newReducer); diff --git a/test/typescript/tsconfig.json b/test/typescript/tsconfig.json new file mode 100644 index 0000000000..e51e60833d --- /dev/null +++ b/test/typescript/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "lib": ["es2015", "dom"], + "strict": true, + "baseUrl": "../..", + "paths": { + "redux": "index.d.ts" + } + } +} \ No newline at end of file