From 0dd93d14dd2b56afa853cb2cdf6bedd7287ed242 Mon Sep 17 00:00:00 2001 From: Tom Crockett Date: Mon, 11 Sep 2017 11:21:43 -0700 Subject: [PATCH 1/5] Revamp TypeScript typing with more type safety --- index.d.ts | 75 ++++++++++++++++++++++--------------- package.json | 2 +- test/typescript/compose.ts | 2 +- test/typescript/reducers.ts | 19 +++++----- test/typescript/store.ts | 2 +- 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/index.d.ts b/index.d.ts index 7c62f0df79..9bcf5776c6 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,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 { + type: T; } - /* reducers */ /** @@ -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 = (state: S | undefined, action: A) => 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; } /** @@ -70,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 */ @@ -92,9 +97,12 @@ 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 S unused, here only for backwards compatibility. + * @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; } /** @@ -109,9 +117,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. * @@ -138,7 +148,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. @@ -182,7 +192,7 @@ export interface Store { * * @param nextReducer The reducer for the store to use instead. */ - replaceReducer(nextReducer: Reducer): void; + replaceReducer(nextReducer: Reducer): void; } /** @@ -191,11 +201,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; } /** @@ -215,10 +227,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. @@ -253,8 +266,8 @@ export const createStore: StoreCreator; /* middleware */ -export interface MiddlewareAPI { - dispatch: Dispatch; +export interface MiddlewareAPI { + dispatch: Dispatch; getState(): S; } @@ -268,7 +281,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; } /** @@ -317,8 +330,8 @@ export interface ActionCreator { /** * Object whose values are action creator functions. */ -export interface ActionCreatorsMapObject { - [key: string]: ActionCreator; +export interface ActionCreatorsMapObject { + [key: string]: ActionCreator; } /** @@ -340,19 +353,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: 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; + >(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; + M extends ActionCreatorsMapObject, + N extends ActionCreatorsMapObject + >(actionCreators: M, dispatch: Dispatch): N; /* compose */ diff --git a/package.json b/package.json index 081a3cb174..0bb7f28fe5 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "rollup-plugin-replace": "^1.1.1", "rollup-plugin-uglify": "^1.0.1", "rxjs": "^5.0.0-beta.6", - "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: [] From 299b0b715b494b9b5c60b6de1859cff8c98af6c6 Mon Sep 17 00:00:00 2001 From: Tom Crockett Date: Mon, 11 Sep 2017 22:14:19 -0700 Subject: [PATCH 2/5] Provide a default action type for combineReducers --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 9bcf5776c6..4025b77b06 100644 --- a/index.d.ts +++ b/index.d.ts @@ -75,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 */ From a76e708c738d9cfaa72a1804f7cbc9417bb670be Mon Sep 17 00:00:00 2001 From: Tom Crockett Date: Tue, 12 Sep 2017 00:02:48 -0700 Subject: [PATCH 3/5] Change state default types to any --- index.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4025b77b06..b5f6982196 100644 --- a/index.d.ts +++ b/index.d.ts @@ -46,14 +46,14 @@ 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. * * @template A The type of actions the reducers can potentially respond to. */ -export type ReducersMapObject = { +export type ReducersMapObject = { [K in keyof S]: Reducer; } @@ -121,7 +121,7 @@ export interface Unsubscribe { * @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. * From 813fbffbff4ec3a1db9a0208395c5fef4f4ab25d Mon Sep 17 00:00:00 2001 From: Tom Crockett Date: Tue, 12 Sep 2017 00:05:51 -0700 Subject: [PATCH 4/5] Don't parameterize Dispatch with a state type --- index.d.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/index.d.ts b/index.d.ts index b5f6982196..55564aa44d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -101,7 +101,7 @@ export function combineReducers(reducers: Reducers * @template S unused, here only for backwards compatibility. * @template D the type of things (actions or otherwise) which may be dispatched. */ -export interface Dispatch { +export interface Dispatch { (action: A): A; } @@ -148,7 +148,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. @@ -267,7 +267,7 @@ export const createStore: StoreCreator; /* middleware */ export interface MiddlewareAPI { - dispatch: Dispatch; + dispatch: Dispatch; getState(): S; } @@ -281,7 +281,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; } /** @@ -353,19 +353,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 */ From 4f5c3200f50329c6f18876a1eecf5aae9950bc81 Mon Sep 17 00:00:00 2001 From: Tom Crockett Date: Tue, 12 Sep 2017 00:08:04 -0700 Subject: [PATCH 5/5] Remove docstring about excised type parameter --- index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 55564aa44d..3e2e960bbf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -98,7 +98,6 @@ export function combineReducers(reducers: Reducers * transform, delay, ignore, or otherwise interpret actions or async actions * before passing them to the next middleware. * - * @template S unused, here only for backwards compatibility. * @template D the type of things (actions or otherwise) which may be dispatched. */ export interface Dispatch {