From 27b98d0580fd39f4b99041191ee57373c71998c5 Mon Sep 17 00:00:00 2001 From: Kyle Herock Date: Sat, 12 Nov 2016 04:29:20 -0500 Subject: [PATCH] More accurate hinted and inferred types for react-redux connect() - Cleaned up and made the typings and tests consistent stylewise - Support typings for the mergeProps parameter - Allow mapStateToProps and mapDispatchToProps to be optional when type parameters are provided - Make static members accessible on the wrapped component - Use stricter typings in the quick start counter tests - Added a single type variable form for connect to specify TOwnProps --- react-redux/index.d.ts | 187 +++++----- react-redux/react-redux-tests.tsx | 594 ++++++++++++++++-------------- 2 files changed, 411 insertions(+), 370 deletions(-) diff --git a/react-redux/index.d.ts b/react-redux/index.d.ts index 3303908a01..ecdd84bb59 100644 --- a/react-redux/index.d.ts +++ b/react-redux/index.d.ts @@ -6,102 +6,119 @@ import * as React from 'react'; import * as Redux from 'redux'; -type ComponentClass

= React.ComponentClass

; -type StatelessComponent

= React.StatelessComponent

; -type ReactNode = React.ReactNode; -type Store = Redux.Store; -type Dispatch = Redux.Dispatch; -type ActionCreator = Redux.ActionCreator; +export = ReactRedux; -interface ComponentDecorator { - (component: ComponentClass | StatelessComponent): ComponentClass; -} +declare namespace ReactRedux { -/** - * Decorator that infers the type from the original component - * - * Can't use the above decorator because it would default the type to {} - */ -export interface InferableComponentDecorator { - | StatelessComponent

)>(component: TComponentConstruct): TComponentConstruct; -} + type ComponentType

= React.ComponentClass

| React.StatelessComponent

; -/** - * Connects a React component to a Redux store. - * - * - Without arguments, just wraps the component, without changing the behavior / props - * - * - If 2 params are passed (3rd param, mergeProps, is skipped), default behavior - * is to override ownProps (as stated in the docs), so what remains is everything that's - * not a state or dispatch prop - * - * - When 3rd param is passed, we don't know if ownProps propagate and whether they - * should be valid component props, because it depends on mergeProps implementation. - * As such, it is the user's responsibility to extend ownProps interface from state or - * dispatch props or both when applicable - * - * @param mapStateToProps - * @param mapDispatchToProps - * @param mergeProps - * @param options - */ -export declare function connect(): InferableComponentDecorator; + interface ComponentDecorator { + >(component: TargetClass): TargetClass & ConnectClass; + } -export declare function connect( - mapStateToProps: FuncOrSelf>, - mapDispatchToProps?: FuncOrSelf | MapDispatchToPropsObject> -): ComponentDecorator; + interface ConnectState { + storeState: S + } -export declare function connect( - mapStateToProps: FuncOrSelf>, - mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject>, - mergeProps: MergeProps, - options?: Options -): ComponentDecorator; + class Connect extends React.Component> { + version: number; + store: Redux.Store; + state: ConnectState; + } -type FuncOrSelf = T | (() => T); + interface ConnectClass extends React.ComponentClass

{ + constructor(props?: P, context?: any): Connect; + WrappedComponent: W; + } -interface MapStateToProps { - (state: any, ownProps?: TOwnProps): TStateProps; -} + export type DefaultStateProps = {}; + export type DefaultDispatchProps = { dispatch: Redux.Dispatch }; + export type DefaultMergedProps = TOwnProps & TStateProps & TDispatchProps; -interface MapDispatchToPropsFunction { - (dispatch: Dispatch, ownProps?: TOwnProps): TDispatchProps; -} - -interface MapDispatchToPropsObject { - [name: string]: ActionCreator; -} - -interface MergeProps { - (stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps): TStateProps & TDispatchProps; -} - -interface Options { /** - * If true, implements shouldComponentUpdate and shallowly compares the result of mergeProps, - * preventing unnecessary updates, assuming that the component is a “pure” component - * and does not rely on any input or state other than its props and the selected Redux store’s state. - * Defaults to true. - * @default true + * Connects a React component to a Redux store. + * + * - Without arguments, just wraps the component, without changing the behavior / props + * + * - If 2 params are passed (3rd param, mergeProps, is skipped), default behavior + * is to override ownProps (as stated in the docs), so what remains is everything that's + * not a state or dispatch prop + * + * - When 3rd param is passed, we don't know if ownProps propagate and whether they + * should be valid component props, because it depends on mergeProps implementation. + * As such, it is the user's responsibility to extend ownProps interface from state or + * dispatch props or both when applicable + * + * @param mapStateToProps + * @param mapDispatchToProps + * @param mergeProps + * @param options */ - pure?: boolean; - /** - * If true, stores a ref to the wrapped component instance and makes it available via - * getWrappedInstance() method. Defaults to false. - */ - withRef?: boolean; -} + export function connect( + mapStateToProps?: FuncOrSelf>, + mapDispatchToProps?: FuncOrSelf | MapDispatchToPropsObject>, + mergeProps?: void, + options?: Options + ): ComponentDecorator; + + export function connect( + mapStateToProps?: FuncOrSelf>, + mapDispatchToProps?: FuncOrSelf | MapDispatchToPropsObject>, + mergeProps?: void, + options?: Options + ): ComponentDecorator, TOwnProps>; + + export function connect( + mapStateToProps?: FuncOrSelf>, + mapDispatchToProps?: FuncOrSelf | MapDispatchToPropsObject>, + mergeProps?: MergeProps, + options?: Options + ): ComponentDecorator; + + type FuncOrSelf = (() => T) | T; + + type MapStateToProps = (state: any, ownProps?: TOwnProps) => TStateProps; + + type InferableMapStateToProps = (state: any, ownProps?: TOwnProps) => TStateProps; + + type MapDispatchToPropsFunction = (dispatch: Redux.Dispatch, ownProps?: TOwnProps) => TDispatchProps; + + type InferableMapDispatchToPropsFunction = (dispatch: Redux.Dispatch, ownProps?: TOwnProps) => TDispatchProps; + + type MapDispatchToPropsObject = { + [name: string]: Redux.ActionCreator; + } + + type MergeProps = + (stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps) => TMergedProps; + + export interface Options { + /** + * If true, implements shouldComponentUpdate and shallowly compares the result of mergeProps, + * preventing unnecessary updates, assuming that the component is a “pure” component + * and does not rely on any input or state other than its props and the selected Redux store’s state. + * Defaults to true. + * @default true + */ + pure?: boolean; + /** + * If true, stores a ref to the wrapped component instance and makes it available via + * getWrappedInstance() method. Defaults to false. + */ + withRef?: boolean; + } + + export interface ProviderProps { + /** + * The single Redux store in your application. + */ + store?: Redux.Store; + children?: React.ReactNode; + } -export interface ProviderProps { /** - * The single Redux store in your application. + * Makes the Redux store available to the connect() calls in the component hierarchy below. */ - store?: Store; - children?: ReactNode; + export class Provider extends React.Component { + } } - -/** - * Makes the Redux store available to the connect() calls in the component hierarchy below. - */ -export class Provider extends React.Component { } diff --git a/react-redux/react-redux-tests.tsx b/react-redux/react-redux-tests.tsx index 8c552c8272..e8aac5dfb3 100644 --- a/react-redux/react-redux-tests.tsx +++ b/react-redux/react-redux-tests.tsx @@ -8,7 +8,7 @@ import { Component, ReactElement } from 'react'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Router, RouterState } from 'react-router'; -import { Store, Dispatch, bindActionCreators } from 'redux'; +import { ActionCreator, Store, Dispatch, bindActionCreators } from 'redux'; import { connect, Provider } from 'react-redux'; import objectAssign = require('object-assign'); import * as History from 'history'; @@ -17,302 +17,324 @@ import * as History from 'history'; // Quick Start // https://github.com/rackt/react-redux/blob/master/docs/quick-start.md#quick-start // - -interface CounterState { - counter: number; -} -declare var increment: Function; - -class Counter extends Component { - render() { - return ( - - ); +namespace TestQuickStartExample { + interface CounterStateProps { + value: number; } -} + interface CounterDispatchProps { + onIncrement: ActionCreator; + } + interface CounterState { + counter: number; + } + declare var increment: Function; -function mapStateToProps(state: CounterState) { - return { + class Counter extends Component { + render() { + return ( + + ); + } + } + + const mapStateToProps = (state: CounterState) => ({ value: state.counter - }; -} - -// Which action creators does it want to receive by props? -function mapDispatchToProps(dispatch: Dispatch) { - return { - onIncrement: () => dispatch(increment()) - }; -} - -connect( - mapStateToProps, - mapDispatchToProps -)(Counter); - - -@connect(mapStateToProps) -class CounterContainer extends Component { - -} - -// Ensure connect's first two arguments can be replaced by wrapper functions -interface ICounterStateProps { - value: number -} -interface ICounterDispatchProps { - onIncrement: () => void -} -connect( - () => mapStateToProps, - () => mapDispatchToProps -)(Counter); -// only first argument -connect( - () => mapStateToProps -)(Counter); -// wrap only one argument -connect( - mapStateToProps, - () => mapDispatchToProps -)(Counter); -// with extra arguments -connect( - () => mapStateToProps, - () => mapDispatchToProps, - (s: ICounterStateProps, d: ICounterDispatchProps) => - objectAssign({}, s, d), - { pure: true } -)(Counter); - - -class App extends Component { - render(): JSX.Element { - // ... - return null; - } -} - -const targetEl = document.getElementById('root'); - -ReactDOM.render(( - - {() => } - -), targetEl); - -// -// API -// https://github.com/rackt/react-redux/blob/master/docs/api.md -// -declare var store: Store; -declare var routerState: RouterState; -declare var history: History.History; -class MyRootComponent extends Component { - -} -class TodoApp extends Component { - -} -interface TodoState { - todos: string[]|string; -} -interface TodoProps { - userId: number; -} -interface DispatchProps { - addTodo(userId: number, text: string): void; - action: Function; -} -declare var actionCreators: () => { - action: Function; -} -declare var addTodo: () => { type: string; }; -declare var todoActionCreators: { [type: string]: (...args: any[]) => any; }; -declare var counterActionCreators: { [type: string]: (...args: any[]) => any; }; - -ReactDOM.render( - - {() => } - , - document.body -); - -//TODO: for React Router 0.13 -////TODO: error TS2339: Property 'run' does not exist on type 'typeof "react-router"'. -////TODO: error TS2339: Property 'HistoryLocation' does not exist on type 'typeof "react-router"'. -//declare var routes: any; -//Router.run(routes, Router.HistoryLocation, (Handler, routerState) => { // note "routerState" here -// ReactDOM.render( -// -// {/* -// //TODO: error TS2339: Property 'routerState' does not exist on type 'RouteProp'. -// {() => } // note "routerState" here: important to pass it down -// */} -// , -// document.getElementById('root') -// ); -//}); - - -//TODO: for React Router 1.0 -ReactDOM.render( - - {() => ...} - , - targetEl -); - -// Inject just dispatch and don't listen to store - -connect()(TodoApp); - -// Inject dispatch and every field in the global state - -connect((state: TodoState) => state)(TodoApp); - -// Inject dispatch and todos - -function mapStateToProps2(state: TodoState) { - return { todos: state.todos }; -} - -export default connect(mapStateToProps2)(TodoApp); - -// Inject todos and all action creators (addTodo, completeTodo, ...) - -//function mapStateToProps(state) { -// return { todos: state.todos }; -//} - -connect(mapStateToProps2, actionCreators)(TodoApp); - -// Inject todos and all action creators (addTodo, completeTodo, ...) as actions - -//function mapStateToProps(state) { -// return { todos: state.todos }; -//} - -function mapDispatchToProps2(dispatch: Dispatch) { - return { actions: bindActionCreators(actionCreators, dispatch) }; -} - -connect(mapStateToProps2, mapDispatchToProps2)(TodoApp); - -// Inject todos and a specific action creator (addTodo) - -//function mapStateToProps(state) { -// return { todos: state.todos }; -//} - -function mapDispatchToProps3(dispatch: Dispatch) { - return bindActionCreators({ addTodo }, dispatch); -} - -connect(mapStateToProps2, mapDispatchToProps3)(TodoApp); - -// Inject todos, todoActionCreators as todoActions, and counterActionCreators as counterActions - -//function mapStateToProps(state) { -// return { todos: state.todos }; -//} - -function mapDispatchToProps4(dispatch: Dispatch) { - return { - todoActions: bindActionCreators(todoActionCreators, dispatch), - counterActions: bindActionCreators(counterActionCreators, dispatch) - }; -} - -connect(mapStateToProps2, mapDispatchToProps4)(TodoApp); - -// Inject todos, and todoActionCreators and counterActionCreators together as actions - -//function mapStateToProps(state) { -// return { todos: state.todos }; -//} - -function mapDispatchToProps5(dispatch: Dispatch) { - return { - actions: bindActionCreators(objectAssign({}, todoActionCreators, counterActionCreators), dispatch) - }; -} - -connect(mapStateToProps2, mapDispatchToProps5)(TodoApp); - -// Inject todos, and all todoActionCreators and counterActionCreators directly as props - -//function mapStateToProps(state) { -// return { todos: state.todos }; -//} - -function mapDispatchToProps6(dispatch: Dispatch) { - return bindActionCreators(objectAssign({}, todoActionCreators, counterActionCreators), dispatch); -} - -connect(mapStateToProps2, mapDispatchToProps6)(TodoApp); - -// Inject todos of a specific user depending on props - -function mapStateToProps3(state: TodoState, ownProps: TodoProps): TodoState { - return { todos: state.todos[ownProps.userId] }; -} - -connect(mapStateToProps3)(TodoApp); - -// Inject todos of a specific user depending on props, and inject props.userId into the action - -//function mapStateToProps(state) { -// return { todos: state.todos }; -//} - -function mergeProps(stateProps: TodoState, dispatchProps: DispatchProps, ownProps: TodoProps): DispatchProps & TodoState & TodoProps { - return objectAssign({}, ownProps, dispatchProps, { - todos: stateProps.todos[ownProps.userId], - addTodo: (text: string) => dispatchProps.addTodo(ownProps.userId, text) }); + + // Which action creators does it want to receive by props? + const mapDispatchToProps = (dispatch: Dispatch) => ({ + onIncrement: () => dispatch(increment()) + }); + + connect( + mapStateToProps, + mapDispatchToProps + )(Counter); + + // https://github.com/Microsoft/TypeScript/issues/4881 + //@connect(mapStateToProps) + //class WrappedCounter extends Component { + //} + //React.createElement(WrappedCounter, {}); + // As a workaround, declare state and dispatch props as optional + interface OptionalCounterStateProps { + value?: number; + } + interface OptionalCounterDispatchProps { + onIncrement?: ActionCreator; + } + interface Props { + requiredOwnProp: string + } + @connect(mapStateToProps) + class WrappedCounter extends Component { + } + React.createElement(WrappedCounter, { requiredOwnProp: 'here I am' }); + + // Ensure connect's first two arguments can be replaced by wrapper functions + connect( + () => mapStateToProps, + () => mapDispatchToProps + )(Counter); + // only first argument + connect( + () => mapStateToProps + )(Counter); + // wrap only one argument + connect( + mapStateToProps, + () => mapDispatchToProps + )(Counter); + // with extra arguments + connect( + () => mapStateToProps, + () => mapDispatchToProps, + (stateProps: CounterStateProps, dispatchProps: CounterDispatchProps) => + objectAssign({}, stateProps, dispatchProps), + { pure: true } + )(Counter); } -connect(mapStateToProps2, actionCreators, mergeProps)(TodoApp); +namespace TestTodosApp { + + class App extends Component { + render(): JSX.Element { + // ... + return null; + } + } + + const targetEl = document.getElementById('root'); + + ReactDOM.render(( + + {() => } + + ), targetEl); + + // + // API + // https://github.com/rackt/react-redux/blob/master/docs/api.md + // + declare var store: Store; + declare var routerState: RouterState; + declare var history: History.History; + class MyRootComponent extends Component<{}, RouterState> { + } + class TodoApp extends Component { + } + interface TodoState { + todos: string[] | string; + } + interface TodoProps { + userId: number; + } + interface DispatchProps { + addTodo(userId: number, text: string): void; + action: Function; + } + declare var actionCreators: () => { + action: Function; + } + declare var addTodo: () => { type: string; }; + declare var todoActionCreators: { [type: string]: (...args: any[]) => any; }; + declare var counterActionCreators: { [type: string]: (...args: any[]) => any; }; + + ReactDOM.render( + + {() => } + , + document.body + ); + + //TODO: for React Router 0.13 + ////TODO: error TS2339: Property 'run' does not exist on type 'typeof "react-router"'. + ////TODO: error TS2339: Property 'HistoryLocation' does not exist on type 'typeof "react-router"'. + //declare var routes: any; + //Router.run(routes, Router.HistoryLocation, (Handler, routerState) => { // note "routerState" here + // ReactDOM.render( + // + // {/* + // //TODO: error TS2339: Property 'routerState' does not exist on type 'RouteProp'. + // {() => } // note "routerState" here: important to pass it down + // */} + // , + // document.getElementById('root') + // ); + //}); + //TODO: for React Router 1.0 + ReactDOM.render( + + {() => ...} + , + targetEl + ); + // Inject just dispatch and don't listen to store + connect()(TodoApp); -interface TestProp { - property1: number; - someOtherProperty?: string; + // Inject dispatch and every field in the global state + + connect((state: TodoState) => state)(TodoApp); + + // Inject dispatch and todos + + function mapStateToProps(state: TodoState) { + return { todos: state.todos }; + } + + connect(mapStateToProps)(TodoApp); + + // Inject todos and all action creators (addTodo, completeTodo, ...) + + //function mapStateToProps(state) { + // return { todos: state.todos }; + //} + + connect(mapStateToProps, actionCreators)(TodoApp); + + // Inject todos and all action creators (addTodo, completeTodo, ...) as actions + + //function mapStateToProps(state) { + // return { todos: state.todos }; + //} + + function mapDispatchToProps(dispatch: Dispatch) { + return { actions: bindActionCreators(actionCreators, dispatch) }; + } + + connect(mapStateToProps, mapDispatchToProps)(TodoApp); + + // Inject todos and a specific action creator (addTodo) + + //function mapStateToProps(state) { + // return { todos: state.todos }; + //} + + function mapDispatchToProps2(dispatch: Dispatch) { + return bindActionCreators({ addTodo }, dispatch); + } + + connect(mapStateToProps, mapDispatchToProps2)(TodoApp); + + // Inject todos, todoActionCreators as todoActions, and counterActionCreators as counterActions + + //function mapStateToProps(state) { + // return { todos: state.todos }; + //} + + function mapDispatchToProps3(dispatch: Dispatch) { + return { + todoActions: bindActionCreators(todoActionCreators, dispatch), + counterActions: bindActionCreators(counterActionCreators, dispatch) + }; + } + + connect(mapStateToProps, mapDispatchToProps3)(TodoApp); + + // Inject todos, and todoActionCreators and counterActionCreators together as actions + + //function mapStateToProps(state) { + // return { todos: state.todos }; + //} + + function mapDispatchToProps4(dispatch: Dispatch) { + return { + actions: bindActionCreators(objectAssign({}, todoActionCreators, counterActionCreators), dispatch) + }; + } + + connect(mapStateToProps, mapDispatchToProps4)(TodoApp); + + // Inject todos, and all todoActionCreators and counterActionCreators directly as props + + //function mapStateToProps(state) { + // return { todos: state.todos }; + //} + + function mapDispatchToProps5(dispatch: Dispatch) { + return bindActionCreators(objectAssign({}, todoActionCreators, counterActionCreators), dispatch); + } + + connect(mapStateToProps, mapDispatchToProps5)(TodoApp); + + // Inject todos of a specific user depending on props + + function mapStateToProps2(state: TodoState, ownProps: TodoProps): TodoState { + return { todos: state.todos[ownProps.userId] }; + } + + connect(mapStateToProps2)(TodoApp); + + // Inject todos of a specific user depending on props, and inject props.userId into the action + + //function mapStateToProps(state) { + // return { todos: state.todos }; + //} + + function mergeProps(stateProps: TodoState, dispatchProps: DispatchProps, ownProps: TodoProps): DispatchProps & TodoState & TodoProps { + return objectAssign({}, ownProps, dispatchProps, { + todos: stateProps.todos[ownProps.userId], + addTodo: (text: string) => dispatchProps.addTodo(ownProps.userId, text) + }); + } + + connect(mapStateToProps, actionCreators, mergeProps)(TodoApp); } -interface TestState { - isLoaded: boolean; - state1: number; + +namespace TestComponent { + declare var store: Store; + interface TestProps { + property1: number; + someOtherProperty?: string; + } + interface TestState { + isLoaded: boolean; + state1: number; + } + class TestComponent extends Component { + static staticMethod(): number { + return 0; + } + } + // Own properties can't be always be inferred, so a single type parameter will do + const WrappedTestComponent = connect()(TestComponent); + + // return value of the connect()(TestComponent) is of the type TestComponent + let ATestComponent: typeof TestComponent = null; + ATestComponent = TestComponent; + ATestComponent = WrappedTestComponent; + // Wrapped component is accessible + WrappedTestComponent.WrappedComponent === TestComponent + // Static properties are accessible + WrappedTestComponent.staticMethod(); + + ; + ; + ; + + class NonComponent { + } + // this doesn't compile + //connect()(NonComponent); + + // stateless functions + interface HelloMessageProps { + name: string; + } + function HelloMessage(props: HelloMessageProps) { + return

Hello {props.name}
; + } + + const ConnectedHelloMessage = connect()(HelloMessage); + ReactDOM.render(, document.getElementById('content')); + ReactDOM.render(, document.getElementById('content')); } -class TestComponent extends Component { } -const WrappedTestComponent = connect()(TestComponent); - -// return value of the connect()(TestComponent) is of the type TestComponent -let ATestComponent: typeof TestComponent = null; -ATestComponent = TestComponent; -ATestComponent = WrappedTestComponent; - -let anElement: ReactElement; -; -; -; - -class NonComponent {} -// this doesn't compile -//connect()(NonComponent); - -// stateless functions -interface HelloMessageProps { name: string; } -function HelloMessage(props: HelloMessageProps) { - return
Hello {props.name}
; -} -let ConnectedHelloMessage = connect()(HelloMessage); -ReactDOM.render(, document.getElementById('content')); -ReactDOM.render(, document.getElementById('content')); // stateless functions that uses mapStateToProps and mapDispatchToProps namespace TestStatelessFunctionWithMapArguments { @@ -343,6 +365,8 @@ namespace TestStatelessFunctionWithMapArguments { mapStateToProps, mapDispatchToProps )(Greeting); + + React.createElement(ConnectedGreeting, { name: 'hello', onClick: () => undefined }); } // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/8787 @@ -374,7 +398,7 @@ namespace TestTOwnPropsInference { const ConnectedWithTypeHint = connect(mapStateToPropsWithoutOwnProps)(OwnPropsComponent); // This compiles, which is bad. - React.createElement(ConnectedWithoutOwnProps, { anything: 'goes!' }); + // React.createElement(ConnectedWithoutOwnProps, { anything: 'goes!' }); // This compiles, as expected. React.createElement(ConnectedWithOwnProps, { own: 'string' });