From 5e221bedd9ed166334bce857f23fac97df116ca4 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Thu, 24 Nov 2016 07:56:58 -0800 Subject: [PATCH] Revert "More accurate hinted and inferred types for react-redux connect()" This reverts commit 27b98d0580fd39f4b99041191ee57373c71998c5. --- react-redux/index.d.ts | 211 +++++------ react-redux/react-redux-tests.tsx | 590 ++++++++++++++---------------- 2 files changed, 380 insertions(+), 421 deletions(-) diff --git a/react-redux/index.d.ts b/react-redux/index.d.ts index ecdd84bb59..3303908a01 100644 --- a/react-redux/index.d.ts +++ b/react-redux/index.d.ts @@ -6,119 +6,102 @@ import * as React from 'react'; import * as Redux from 'redux'; -export = ReactRedux; +type ComponentClass

= React.ComponentClass

; +type StatelessComponent

= React.StatelessComponent

; +type ReactNode = React.ReactNode; +type Store = Redux.Store; +type Dispatch = Redux.Dispatch; +type ActionCreator = Redux.ActionCreator; -declare namespace ReactRedux { - - type ComponentType

= React.ComponentClass

| React.StatelessComponent

; - - interface ComponentDecorator { - >(component: TargetClass): TargetClass & ConnectClass; - } - - interface ConnectState { - storeState: S - } - - class Connect extends React.Component> { - version: number; - store: Redux.Store; - state: ConnectState; - } - - interface ConnectClass extends React.ComponentClass

{ - constructor(props?: P, context?: any): Connect; - WrappedComponent: W; - } - - export type DefaultStateProps = {}; - export type DefaultDispatchProps = { dispatch: Redux.Dispatch }; - export type DefaultMergedProps = TOwnProps & TStateProps & TDispatchProps; - - /** - * 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 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; - } - - /** - * Makes the Redux store available to the connect() calls in the component hierarchy below. - */ - export class Provider extends React.Component { - } +interface ComponentDecorator { + (component: ComponentClass | StatelessComponent): ComponentClass; } + +/** + * 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; +} + +/** + * 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; + +export declare function connect( + mapStateToProps: FuncOrSelf>, + mapDispatchToProps?: FuncOrSelf | MapDispatchToPropsObject> +): ComponentDecorator; + +export declare function connect( + mapStateToProps: FuncOrSelf>, + mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject>, + mergeProps: MergeProps, + options?: Options +): ComponentDecorator; + +type FuncOrSelf = T | (() => T); + +interface MapStateToProps { + (state: any, ownProps?: TOwnProps): TStateProps; +} + +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 + */ + 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?: Store; + children?: ReactNode; +} + +/** + * 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 e8aac5dfb3..8c552c8272 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 { ActionCreator, Store, Dispatch, bindActionCreators } from 'redux'; +import { Store, Dispatch, bindActionCreators } from 'redux'; import { connect, Provider } from 'react-redux'; import objectAssign = require('object-assign'); import * as History from 'history'; @@ -17,324 +17,302 @@ import * as History from 'history'; // Quick Start // https://github.com/rackt/react-redux/blob/master/docs/quick-start.md#quick-start // -namespace TestQuickStartExample { - interface CounterStateProps { - value: number; - } - interface CounterDispatchProps { - onIncrement: ActionCreator; - } - interface CounterState { - counter: number; - } - declare var increment: Function; - class Counter extends Component { - render() { - return ( - - ); - } - } +interface CounterState { + counter: number; +} +declare var increment: Function; - const mapStateToProps = (state: CounterState) => ({ +class Counter extends Component { + render() { + return ( + + ); + } +} + +function mapStateToProps(state: CounterState) { + return { value: state.counter - }); + }; +} - // Which action creators does it want to receive by props? - const mapDispatchToProps = (dispatch: Dispatch) => ({ +// 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) }); - - 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); } -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') - // ); - //}); +connect(mapStateToProps2, actionCreators, mergeProps)(TodoApp); - //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 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 TestProp { + property1: number; + someOtherProperty?: string; } - -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')); +interface TestState { + isLoaded: boolean; + state1: number; } +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 { @@ -365,8 +343,6 @@ namespace TestStatelessFunctionWithMapArguments { mapStateToProps, mapDispatchToProps )(Greeting); - - React.createElement(ConnectedGreeting, { name: 'hello', onClick: () => undefined }); } // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/8787 @@ -398,7 +374,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' });