From 9c45235094790427cebc4eb95ff675ec1bc4707a Mon Sep 17 00:00:00 2001 From: warreq Date: Fri, 11 May 2018 00:24:09 +0200 Subject: [PATCH] Infer RootState type from RootReducer Use ReturnType (introduced in TypeScript 2.8) to infer RootState's type directly from the map used for constructing the RootReducer. This prevents us from having to repeat ourselves, so when we add a new reducer, we only need to add it in one place, so we can't accidentally add it to the RootState type but forget to update the reducer itself or vice-versa. Upgrading the playground project to TS 2.8 caused issues with two of the HOC examples because of their use of the spread operator, which I was only able to resolve by using Object.assign in one case, and using `as any` in another. Hopefully the TypeScript team will fix this issue soon by introducing the `Spread` type. --- README.md | 33 +++++++++++----------- docs/markdown/2_redux.md | 4 ++- playground/package.json | 2 +- playground/src/hoc/with-error-boundary.tsx | 2 +- playground/src/hoc/with-state.tsx | 2 +- playground/src/redux/root-reducer.ts | 25 ++++++++-------- playground/yarn.lock | 6 ++-- 7 files changed, 38 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 94e9dfc..21e9892 100644 --- a/README.md +++ b/README.md @@ -479,7 +479,7 @@ export const withState =

( } render() { - const { ...remainingProps } = this.props; + const remainingProps = Object.assign({}, this.props); const { count } = this.state; return ( @@ -556,7 +556,7 @@ export const withErrorBoundary =

( } render() { - const { children, ...remainingProps } = this.props; + const { children, ...remainingProps } = this.props as any; const { error } = this.state; if (error) { @@ -958,29 +958,30 @@ describe('Todos Logic', () => { ### Create Root State and Root Action Types #### `RootState` - interface representing redux state tree -Can be imported in connected components to provide type-safety to Redux `connect` function +Can be imported in connected components to provide type-safety to Redux `connect` function. + +Because the RootState is the sum of it's reducers' return types (plus any store Enhancers we may choose to append), TypeScript can infer its shape entirely from Lookup Types and `ReturnType<>`. ```tsx import { combineReducers } from 'redux'; -import { routerReducer, RouterState } from 'react-router-redux'; - -import { countersReducer, CountersState } from '@src/redux/counters'; -import { todosReducer, TodosState } from '@src/redux/todos'; +import { routerReducer } from 'react-router-redux'; -interface StoreEnhancerState { } - -export interface RootState extends StoreEnhancerState { - router: RouterState; - counters: CountersState; - todos: TodosState; -} +import { countersReducer } from '@src/redux/counters'; +import { todosReducer} from '@src/redux/todos'; import { RootAction } from '@src/redux'; -export const rootReducer = combineReducers({ + +const reducerMap = { router: routerReducer, counters: countersReducer, todos: todosReducer, -}); +}; + +interface StoreEnhancerState { } + +export type RootState = { [K in keyof typeof reducerMap]: ReturnType } & StoreEnhancerState; + +export const rootReducer = combineReducers(reducerMap); ``` diff --git a/docs/markdown/2_redux.md b/docs/markdown/2_redux.md index 0112fcd..91e9d72 100644 --- a/docs/markdown/2_redux.md +++ b/docs/markdown/2_redux.md @@ -93,7 +93,9 @@ state.counterPairs[0].immutableCounter2 = 1; // Error, cannot be mutated ### Create Root State and Root Action Types #### `RootState` - interface representing redux state tree -Can be imported in connected components to provide type-safety to Redux `connect` function +Can be imported in connected components to provide type-safety to Redux `connect` function. + +Because the RootState is the sum of it's reducers' return types (plus any store Enhancers we may choose to append), TypeScript can infer its shape entirely from Lookup Types and `ReturnType<>`. ::example='../../playground/src/redux/root-reducer.ts':: diff --git a/playground/package.json b/playground/package.json index a1b9850..6a3de48 100644 --- a/playground/package.json +++ b/playground/package.json @@ -56,7 +56,7 @@ "ts-jest": "22.0.1", "tslint": "5.8.0", "tslint-react": "3.3.3", - "typescript": "2.7.2", + "typescript": "2.8.3", "webpack": "3.10.0", "webpack-blocks": "1.0.0-rc.2" } diff --git a/playground/src/hoc/with-error-boundary.tsx b/playground/src/hoc/with-error-boundary.tsx index b3b11f1..c113c86 100644 --- a/playground/src/hoc/with-error-boundary.tsx +++ b/playground/src/hoc/with-error-boundary.tsx @@ -36,7 +36,7 @@ export const withErrorBoundary =

( } render() { - const { children, ...remainingProps } = this.props; + const { children, ...remainingProps } = this.props as any; const { error } = this.state; if (error) { diff --git a/playground/src/hoc/with-state.tsx b/playground/src/hoc/with-state.tsx index d891a4b..fef2e38 100644 --- a/playground/src/hoc/with-state.tsx +++ b/playground/src/hoc/with-state.tsx @@ -31,7 +31,7 @@ export const withState =

( } render() { - const { ...remainingProps } = this.props; + const remainingProps = Object.assign({}, this.props); const { count } = this.state; return ( diff --git a/playground/src/redux/root-reducer.ts b/playground/src/redux/root-reducer.ts index 5720aa8..e208aa7 100644 --- a/playground/src/redux/root-reducer.ts +++ b/playground/src/redux/root-reducer.ts @@ -1,20 +1,19 @@ import { combineReducers } from 'redux'; -import { routerReducer, RouterState } from 'react-router-redux'; +import { routerReducer } from 'react-router-redux'; -import { countersReducer, CountersState } from '@src/redux/counters'; -import { todosReducer, TodosState } from '@src/redux/todos'; - -interface StoreEnhancerState { } - -export interface RootState extends StoreEnhancerState { - router: RouterState; - counters: CountersState; - todos: TodosState; -} +import { countersReducer } from '@src/redux/counters'; +import { todosReducer} from '@src/redux/todos'; import { RootAction } from '@src/redux'; -export const rootReducer = combineReducers({ + +const reducerMap = { router: routerReducer, counters: countersReducer, todos: todosReducer, -}); +}; + +interface StoreEnhancerState { } + +export type RootState = { [K in keyof typeof reducerMap]: ReturnType } & StoreEnhancerState; + +export const rootReducer = combineReducers(reducerMap); diff --git a/playground/yarn.lock b/playground/yarn.lock index bc27761..8ac0886 100644 --- a/playground/yarn.lock +++ b/playground/yarn.lock @@ -7211,9 +7211,9 @@ typesafe-actions@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/typesafe-actions/-/typesafe-actions-1.1.2.tgz#af88ede3ee254be425c3e0e02de11b182830ae48" -typescript@2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836" +typescript@2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.3.tgz#5d817f9b6f31bb871835f4edf0089f21abe6c170" typescript@^2.4.2: version "2.6.2"