From 169d34730ae9b76779a6fcadbd538d4bb00c5a47 Mon Sep 17 00:00:00 2001 From: Tom Crockett Date: Thu, 5 Oct 2017 18:04:20 -0700 Subject: [PATCH] Revamp TypeScript typing with more type safety (#2563) * Revamp TypeScript typing with more type safety * Provide a default action type for combineReducers * Change state default types to any * Don't parameterize Dispatch with a state type * Remove docstring about excised type parameter --- index.d.ts | 80 +++++++++++++++++++------------------ package.json | 2 +- test/typescript/compose.ts | 2 +- test/typescript/reducers.ts | 19 ++++----- test/typescript/store.ts | 2 +- 5 files changed, 54 insertions(+), 51 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1009ce85e2..3e2e960bbf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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, @@ -12,22 +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; -} - -/** - * 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; +export interface Action { + type: T; } - /* reducers */ /** @@ -51,15 +43,18 @@ export interface AnyAction extends 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 = (state: S, action: AnyAction) => S; +export type Reducer = (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 type ReducersMapObject = { - [K in keyof S]: Reducer; +export type ReducersMapObject = { + [K in keyof S]: Reducer; } /** @@ -80,7 +75,7 @@ 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; /* store */ @@ -102,9 +97,11 @@ export function combineReducers(reducers: ReducersMapObject): Reducer; * 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 { - (action: A): A; +export interface Dispatch { + (action: A): A; } /** @@ -119,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 { +export interface Store { /** * Dispatches an action. It is the only way to trigger a state change. * @@ -148,7 +147,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. @@ -192,7 +191,7 @@ export interface Store { * * @param nextReducer The reducer for the store to use instead. */ - replaceReducer(nextReducer: Reducer): void; + replaceReducer(nextReducer: Reducer): void; } /** @@ -201,11 +200,13 @@ export interface Store { * `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 { - (reducer: Reducer, enhancer?: StoreEnhancer): Store; - (reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; + (reducer: Reducer, enhancer?: StoreEnhancer): Store; + (reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; } /** @@ -225,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 = (next: StoreEnhancerStoreCreator) => StoreEnhancerStoreCreator; -export type GenericStoreEnhancer = (next: StoreEnhancerStoreCreator) => StoreEnhancerStoreCreator; -export type StoreEnhancerStoreCreator = (reducer: Reducer, preloadedState?: S) => Store; +export type StoreEnhancer = (next: StoreEnhancerStoreCreator) => StoreEnhancerStoreCreator; +export type GenericStoreEnhancer = StoreEnhancer; +export type StoreEnhancerStoreCreator = (reducer: Reducer, preloadedState?: S) => Store; /** * Creates a Redux store that holds the state tree. @@ -263,8 +265,8 @@ export const createStore: StoreCreator; /* middleware */ -export interface MiddlewareAPI { - dispatch: Dispatch; +export interface MiddlewareAPI { + dispatch: Dispatch; getState(): S; } @@ -278,7 +280,7 @@ export interface MiddlewareAPI { * asynchronous API call into a series of synchronous actions. */ export interface Middleware { - (api: MiddlewareAPI): (next: Dispatch) => Dispatch; + (api: MiddlewareAPI): (next: Dispatch) => Dispatch; } /** @@ -327,8 +329,8 @@ export interface ActionCreator { /** * Object whose values are action creator functions. */ -export interface ActionCreatorsMapObject { - [key: string]: ActionCreator; +export interface ActionCreatorsMapObject { + [key: string]: ActionCreator; } /** @@ -350,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>(actionCreator: A, dispatch: Dispatch): A; +export function bindActionCreators>(actionCreator: C, dispatch: Dispatch): C; 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>(actionCreators: M, dispatch: Dispatch): M; export function bindActionCreators< - M extends ActionCreatorsMapObject, - N extends ActionCreatorsMapObject + M extends ActionCreatorsMapObject, + N extends ActionCreatorsMapObject >(actionCreators: M, dispatch: Dispatch): N; diff --git a/package.json b/package.json index cde5b87928..ff4a15e8ff 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "rollup-plugin-replace": "^2.0.0", "rollup-plugin-uglify": "^2.0.1", "rxjs": "^5.5.0", - "typescript": "^2.1.0", + "typescript": "^2.4.2", "typescript-definition-tester": "0.0.5" }, "npmName": "redux", diff --git a/test/typescript/compose.ts b/test/typescript/compose.ts index 0866f541af..d48c9c64ca 100644 --- a/test/typescript/compose.ts +++ b/test/typescript/compose.ts @@ -36,4 +36,4 @@ const t11: number = compose(stringToNumber, numberToString, stringToNumber, const funcs = [stringToNumber, numberToString, stringToNumber]; -const t12 = compose(...funcs)('bar', 42, true); +const t12 = compose(...funcs)('bar'); diff --git a/test/typescript/reducers.ts b/test/typescript/reducers.ts index ebf17c849e..0fc3c51367 100644 --- a/test/typescript/reducers.ts +++ b/test/typescript/reducers.ts @@ -11,15 +11,15 @@ interface AddTodoAction extends Action { } -const todosReducer: Reducer = (state: TodosState, - action: Action): TodosState => { - switch (action.type) { - case 'ADD_TODO': - return [...state, (action).text] - default: - return state +const todosReducer: Reducer = + (state = [], action) => { + switch (action.type) { + case 'ADD_TODO': + return [...state, action.text] + default: + return state + } } -} const todosState: TodosState = todosReducer([], { type: 'ADD_TODO', @@ -47,7 +47,8 @@ type RootState = { counter: CounterState; } -const rootReducer: Reducer = combineReducers({ + +const rootReducer = combineReducers({ todos: todosReducer, counter: counterReducer, }) diff --git a/test/typescript/store.ts b/test/typescript/store.ts index b38bdb7efb..b00b7d2cdc 100644 --- a/test/typescript/store.ts +++ b/test/typescript/store.ts @@ -15,7 +15,7 @@ const reducer: Reducer = (state: State, action: Action): State => { /* createStore */ -const store: Store = createStore(reducer); +const store: Store = createStore(reducer); const storeWithPreloadedState: Store = createStore(reducer, { todos: []