diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000..355dc25ece --- /dev/null +++ b/index.d.ts @@ -0,0 +1,221 @@ +import { + Action, + ActionCreator, + Reducer, + AnyAction, + Middleware, + StoreEnhancer, + ReducersMapObject, + Store +} from 'redux' + +export { + Action, + ActionCreator, + AnyAction, + Middleware, + Reducer, + ReducersMapObject, + Store, + StoreEnhancer, + combineReducers, + compose +} from 'redux' +export { default as createSelector } from 'selectorator' +export { produce as createNextState } from 'immer' + +/* configureStore() */ + +/** + * A configuration object that can be passed to `configureStore()`. + */ +export interface StoreConfiguration { + /** + * A single reducer function that will be used as the root reducer, or an + * object of slice reducers that will be passed to `combineReducers()`. + */ + reducer: Reducer | ReducersMapObject + + /** + * An array of Redux middlewares. If not supplied, defaults to just + * `redux-thunk`. + */ + middleware?: Middleware[] + + /** + * Whether to enable Redux DevTools integration. Defaults to `true`. + */ + devTools?: boolean + + /** + * 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 (either directly or indirectly by passing an object as `reducer`), + * this must be an object with the same shape as the reducer map keys. + */ + preloadedState?: S + + /** + * The store enhancer. See `createStore()`. If you only need to add + * middleware, you can use the `middleware` parameter instaead. + */ + enhancer?: StoreEnhancer +} + +/** + * A friendlier abstraction over the standard Redux `createStore()` function. + * + * @param config The store configuration. + * @returns A configured Redux store. + */ +export function configureStore( + config: StoreConfiguration +): Store + +/* getDefaultMiddleware() */ + +/** + * Returns any array containing the default middleware installed by + * `configureStore`. Useful if you want to configure your store with a custom + * `middleware` array but still keep the default set. + */ +export function getDefaultMiddleware(): Middleware[] + +/* createAction() */ + +/** + * An action with an associated payload. The type of action returned by + * action creators that are generated using {@link createAction}. + * + * @template P The type of the action's payload. + * @template T the type of the action's `type` tag. + */ +export interface PayloadAction

extends Action { + payload: P +} + +export interface PayloadActionCreator

{ + (): Action + (payload: P): PayloadAction +} + +/** + * A utility function to create an action creator for the given action type + * string. The action creator accepts a single argument, which will be included + * in the action object as a field called payload. The action creator function + * will also have its toString() overriden so that it returns the action type, + * allowing it to be used in reducer logic that is looking for that action type. + * + * @param type + */ +export function createAction

( + type: T +): PayloadActionCreator + +/* createReducer() */ + +/** + * An *action handler* is a reducer for a speficic action type passed to + * `createReducer()`. In contrast to a normal Redux reducer, it is never + * called with an `undefined` state because the initial state is explicitly + * passed as the first argument to `createReducer()`. + */ +export interface ActionHandler { + (state: S, action: A): S +} + +/** + * A mapping from action types to action handlers, meant to be passed to + * `createReducer()`. + */ +export interface ActionHandlersMapObject { + [actionType: string]: ActionHandler +} + +/** + * A utility function to create reducers that handle specific action types. + * case reducer functions. Internally, it uses the `immer` library, so you + * can write code in your case reducers that mutates the existing state value, + * and it will correctly generate immutably-updated state values instead. + * + * @param initialState The initial state to be returned by the reducer. + * @param actionsMap A mapping from action types to action handlers + * (action-type-specific reducer functions). + */ +export function createReducer( + initialState: S, + actionsMap: ActionHandlersMapObject +): Reducer + +/* createSlice() */ + +/** + * A *slice* bundles a reducer, creators for the actions handled by that + * reducer, and selectors for the reducer's state. + */ +export interface Slice< + S, + A extends Action = AnyAction, + AT extends string = string +> { + /** + * The slice's reducer. + */ + reducer: Reducer + + /** + * Action creators for the types of actions that are handled by the slice + * reducer. + */ + actions: { [type in AT]: ActionCreator } + + /** + * Selectors for the slice reducer state. `createSlice()` inserts a single + * selector that returns the entire slice state and whose name is + * automatically derived from the slice name (e.g., `getCounter` for a slice + * named `counter`). + */ + selectors: { [key: string]: (state: any) => S } +} + +/** + * A configuration object for `createSlice()`. + */ +export interface SliceConfiguration< + S, + A extends Action = AnyAction, + AT extends string = string +> { + /** + * The slice's name. Used to namespace the generated action types and to + * name the selector for retrieving the reducer's state. + */ + slice?: string + + /** + * The initial state to be returned by the slice reducer. + */ + initialState: S + + /** + * A mapping from action types to handlers (reducer functions) for these + * actions. For every action type, a matching action creator will be + * generated. + */ + reducers: { [key in AT]: ActionHandler } +} + +/** + * A function that accepts an initial state, an object full of reducer + * functions, and optionally a "slice name", and automatically generates + * action creators, action types, and selectors that correspond to the + * reducers and state. + * + * The reducers are wrapped with `createReducer()`. + */ +export function createSlice< + S, + A extends Action = AnyAction, + AT extends string = string +>(config: SliceConfiguration): Slice diff --git a/package-lock.json b/package-lock.json index 80ea00fc97..ef41c38473 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2635,14 +2635,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2657,20 +2655,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2787,8 +2782,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2800,7 +2794,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2815,7 +2808,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2823,14 +2815,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -2849,7 +2839,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2930,8 +2919,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -2943,7 +2931,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3065,7 +3052,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7127,6 +7113,21 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz", + "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==", + "dev": true + }, + "typings-tester": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/typings-tester/-/typings-tester-0.3.2.tgz", + "integrity": "sha512-HjGoAM2UoGhmSKKy23TYEKkxlphdJFdix5VvqWFLzH1BJVnnwG38tpC6SXPgqhfFGfHY77RlN1K8ts0dbWBQ7A==", + "dev": true, + "requires": { + "commander": "^2.12.2" + } + }, "uglify-js": { "version": "3.4.9", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", diff --git a/package.json b/package.json index 61ec2e8b08..c72952726f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "rollup-plugin-babel": "^3.0.3", "rollup-plugin-commonjs": "^8.3.0", "rollup-plugin-node-resolve": "^3.0.3", - "rollup-plugin-replace": "^2.1.0" + "rollup-plugin-replace": "^2.1.0", + "typescript": "^3.2.2", + "typings-tester": "^0.3.2" }, "scripts": { "build": "rollup -c", @@ -37,7 +39,8 @@ "test": "jest" }, "files": [ - "dist" + "dist", + "index.d.ts" ], "dependencies": { "immer": "^1.9.3", diff --git a/typing-tests/typescript.test.js b/typing-tests/typescript.test.js new file mode 100644 index 0000000000..632b51cd3a --- /dev/null +++ b/typing-tests/typescript.test.js @@ -0,0 +1,5 @@ +import { checkDirectory } from 'typings-tester' + +test('TypeScript definitions', () => { + checkDirectory(`${__dirname}/typescript`) +}) diff --git a/typing-tests/typescript/configureStore.ts b/typing-tests/typescript/configureStore.ts new file mode 100644 index 0000000000..df2f1cf36e --- /dev/null +++ b/typing-tests/typescript/configureStore.ts @@ -0,0 +1,124 @@ +import { applyMiddleware } from 'redux' +import { + AnyAction, + configureStore, + Middleware, + PayloadAction, + Reducer, + Store +} from 'redux-starter-kit' + +/* + * Test: configureStore() requires a valid reducer or reducer map. + */ +{ + configureStore({ + reducer: (state, action) => 0 + }) + + configureStore({ + reducer: { + counter1: () => 0, + counter2: () => 1 + } + }) + + // typings:expect-error + configureStore({ reducer: 'not a reducer' }) + + // typings:expect-error + configureStore({ reducer: { a: 'not a reducer' } }) + + // typings:expect-error + configureStore({}) +} + +/* + * Test: configureStore() infers the store state type. + */ +{ + const reducer: Reducer = () => 0 + const store = configureStore({ reducer }) + const numberStore: Store = store + + // typings:expect-error + const stringStore: Store = store +} + +/* + * Test: configureStore() infers the store action type. + */ +{ + const reducer: Reducer> = () => 0 + const store = configureStore({ reducer }) + const numberStore: Store> = store + + // typings:expect-error + const stringStore: Store> = store +} + +/* + * Test: configureStore() accepts middleware array. + */ +{ + const middleware: Middleware = store => next => next + + configureStore({ + reducer: () => 0, + middleware: [middleware] + }) + + // typings:expect-error + configureStore({ + reducer: () => 0, + middleware: ['not middleware'] + }) +} + +/* + * Test: configureStore() accepts devTools flag. + */ +{ + configureStore({ + reducer: () => 0, + devTools: true + }) + + // typings:expect-error + configureStore({ + reducer: () => 0, + devTools: 'true' + }) +} + +/* + * Test: configureStore() accepts preloadedState. + */ +{ + configureStore({ + reducer: () => 0, + preloadedState: 0 + }) + + // typings:expect-error + configureStore({ + reducer: () => 0, + preloadedState: 'non-matching state type' + }) +} + +/* + * Test: configureStore() accepts store enhancer. + */ +{ + configureStore({ + reducer: () => 0, + enhancer: applyMiddleware(store => next => next) + }) + + // typings:expect-error + configureStore({ + reducer: () => 0, + enhancer: 'not a store enhancer' + }) +} diff --git a/typing-tests/typescript/createAction.ts b/typing-tests/typescript/createAction.ts new file mode 100644 index 0000000000..40c3178de9 --- /dev/null +++ b/typing-tests/typescript/createAction.ts @@ -0,0 +1,99 @@ +import { + createAction, + PayloadAction, + ActionCreator, + PayloadActionCreator, + Action, + AnyAction +} from 'redux-starter-kit' + +/* PayloadAction */ + +/* + * Test: PayloadAction has type parameter for the payload. + */ +{ + const action: PayloadAction = { type: '', payload: 5 } + const numberPayload: number = action.payload + + // typings:expect-error + const stringPayload: string = action.payload +} + +/* + * Test: PayloadAction type parameter is optional (defaults to `any`). + */ +{ + const action: PayloadAction = { type: '', payload: 5 } + const numberPayload: number = action.payload + const stringPayload: string = action.payload +} + +/* + * Test: PayloadAction has optional second type parameter for the type tag. + */ +{ + const action: PayloadAction = { type: '', payload: 5 } + + // typings:expect-error + const action2: PayloadAction = { type: 1, payload: 5 } + + // typings:expect-error + const action3: PayloadAction = { type: '', payload: 5 } +} + +/* PayloadActionCreator */ + +/* + * Test: PayloadActionCreator returns Action or PayloadAction depending + * on whether a payload is passed. + */ +{ + const actionCreator: PayloadActionCreator = (payload?: number) => ({ + type: 'action', + payload + }) + + let action: Action + let payloadAction: PayloadAction + + action = actionCreator() + action = actionCreator(1) + payloadAction = actionCreator(1) + + // typings:expect-error + payloadAction = actionCreator() +} + +/* + * Test: PayloadActionCreator is compatible with ActionCreator. + */ +{ + const payloadActionCreator: PayloadActionCreator = (payload?: number) => ({ + type: 'action', + payload + }) + const actionCreator: ActionCreator = payloadActionCreator +} + +/* createAction() */ + +/* + * Test: createAction() has type parameter for the action payload. + */ +{ + const increment = createAction('increment') + const n: number = increment(1).payload + + // typings:expect-error + const s: string = increment(1).payload +} + +/* + * Test: createAction() type parameter is optional (defaults to `any`). + */ +{ + const increment = createAction('increment') + const n: number = increment(1).payload + const s: string = increment(1).payload +} diff --git a/typing-tests/typescript/createReducer.ts b/typing-tests/typescript/createReducer.ts new file mode 100644 index 0000000000..806e275579 --- /dev/null +++ b/typing-tests/typescript/createReducer.ts @@ -0,0 +1,58 @@ +import { AnyAction, createReducer, Reducer } from 'redux-starter-kit' + +/* + * Test: createReducer() infers type of returned reducer. + */ +{ + type CounterAction = + | { type: 'increment'; payload: number } + | { type: 'decrement'; payload: number } + + const incrementHandler = (state: number, action: CounterAction) => state + 1 + const decrementHandler = (state: number, action: CounterAction) => state - 1 + + const reducer = createReducer(0, { + increment: incrementHandler, + decrement: decrementHandler + }) + + const numberReducer: Reducer = reducer + + // typings:expect-error + const stringReducer: Reducer = reducer + + // typings:expect-error + const anyActionReducer: Reducer = reducer +} + +/** + * Test: createReducer() type parameters can be specified expliclity. + */ +{ + type CounterAction = + | { type: 'increment'; payload: number } + | { type: 'decrement'; payload: number } + + const incrementHandler = (state: number, action: CounterAction) => + state + action.payload + + const decrementHandler = (state: number, action: CounterAction) => + state - action.payload + + createReducer(0, { + increment: incrementHandler, + decrement: decrementHandler + }) + + // typings:expect-error + createReducer(0, { + increment: incrementHandler, + decrement: decrementHandler + }) + + // typings:expect-error + createReducer(0, { + increment: incrementHandler, + decrement: decrementHandler + }) +} diff --git a/typing-tests/typescript/createSlice.ts b/typing-tests/typescript/createSlice.ts new file mode 100644 index 0000000000..a7e486c969 --- /dev/null +++ b/typing-tests/typescript/createSlice.ts @@ -0,0 +1,46 @@ +import { + AnyAction, + createSlice, + PayloadAction, + Reducer +} from 'redux-starter-kit' + +/* + * Test: createSlice() infers the returned slice's type. + */ +{ + const slice = createSlice({ + slice: 'counter', + initialState: 0, + reducers: { + increment: (state: number, action: PayloadAction) => + state + action.payload, + decrement: (state: number, action: PayloadAction) => + state - action.payload + } + }) + + /* Reducer */ + + const reducer: Reducer = slice.reducer + + // typings:expect-error + const stringReducer: Reducer = slice.reducer + // typings:expect-error + const anyActionReducer: Reducer = slice.reducer + + /* Actions */ + + slice.actions.increment(1) + slice.actions.decrement(1) + + // typings:expect-error + slice.actions.other(1) + + /* Selector */ + + const value: number = slice.selectors.getCounter(0) + + // typings:expect-error + const stringValue: string = slice.selectors.getCounter(0) +} diff --git a/typing-tests/typescript/tsconfig.json b/typing-tests/typescript/tsconfig.json new file mode 100644 index 0000000000..75025b4460 --- /dev/null +++ b/typing-tests/typescript/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "lib": ["es2015"], + "strict": true, + "baseUrl": "../..", + "paths": { + "redux-starter-kit": ["index.d.ts"] + } + } +}