[react-redux]: decoration target props should not extend injected props, contravariance issue (#25228)

* feat(react-redux): add strict null check

* feat(react-redux): remove improper inference tests

Infered decorated typings would rely on the implicit and false
assumption that decorated component props extend injected props and
own props

#24922
#24913

* fix(react-redux): injected props and decorated component props intersection

* InjectedProps should not have to extend DecoratedProps
* DecoratedProps should not have to extend InjectedProps
* DecoratedProps should extend Intersection<InjectedProps, DecoratedProps>
* Remaining Props should be required on the decoration output

#24913
#24922

* feat(react-redux): add new commiters

* feat(react-redux): 2.9 keyof compatibility depends on Extract (2.8)
This commit is contained in:
Thomas Charlat
2018-05-07 21:34:04 +02:00
committed by Sheetal Nandi
parent d12082bfdf
commit 65b176eaef
13 changed files with 109 additions and 43 deletions

View File

@@ -2,7 +2,7 @@
// Project: https://github.com/mirrorjs/mirror
// Definitions by: Aaronphy <https://github.com/aaronphy>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import * as H from 'history';

View File

@@ -2,7 +2,7 @@
// Project: https://github.com/kirill-konshin/next-redux-wrapper
// Definitions by: Steve <https://github.com/stevegeek>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
/// <reference types="node" />
/*~ Note that ES6 modules cannot directly export callable functions.

View File

@@ -2,7 +2,7 @@
// Project: https://github.com/ratson/react-intl-redux
// Definitions by: Karol Janyst <https://github.com/LKay>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import { Action } from "redux"
import { Provider as ReduxProvider } from "react-redux"

View File

@@ -4,7 +4,7 @@
// Artyom Stukans <https://github.com/artyomsv>
// Mika Kuitunen <https://github.com/kulmajaba>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import { Component } from 'react';
import { Action, ActionCreator, Reducer } from 'redux';

View File

@@ -8,8 +8,11 @@
// Nicholas Boll <https://github.com/nicholasboll>
// Dibyo Majumdar <https://github.com/mdibyo>
// Prashant Deva <https://github.com/pdeva>
// Thomas Charlat <https://github.com/kallikrein>
// Valentin Descamps <https://github.com/val1984>
// Johann Rakotoharisoa <https://github.com/jrakotoharisoa>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
// Known Issue:
// There is a known issue in TypeScript, which doesn't allow decorators to change the signature of the classes
@@ -37,21 +40,39 @@ type ActionCreator<A> = Redux.ActionCreator<A>;
// Diff / Omit taken from https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766
type Omit<T, K extends keyof T> = Pick<T, ({ [P in keyof T]: P } & { [P in K]: never } & { [x: string]: never, [x: number]: never })[keyof T]>;
export interface DispatchProp<A extends Redux.Action = Redux.AnyAction> {
dispatch?: Dispatch<A>;
dispatch: Dispatch<A>;
}
interface AdvancedComponentDecorator<TProps, TOwnProps> {
(component: Component<TProps>): ComponentClass<TOwnProps>;
}
/**
* a property P will be present if :
* - it is present in both DecorationTargetProps and InjectedProps
* - DecorationTargetProps[P] extends InjectedProps[P]
* ie: decorated component can accept more types than decorator is injecting
*
* For decoration, inject props or ownProps are all optionnaly
* required by the decorated (right hand side) component.
* But any property required by the decorated component must extend the injected property
*/
type Shared<
InjectedProps,
DecorationTargetProps extends Shared<InjectedProps, DecorationTargetProps>
> = {
[P in Extract<keyof InjectedProps, keyof DecorationTargetProps>]?: DecorationTargetProps[P] extends InjectedProps[P] ? InjectedProps[P] : never;
};
// Injects props and removes them from the prop requirements.
// Will not pass through the injected props if they are passed in during
// render. Also adds new prop requirements from TNeedsProps.
export interface InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> {
<P extends TInjectedProps>(
<P extends Shared<TInjectedProps, P>>(
component: Component<P>
): ComponentClass<Omit<P, keyof TInjectedProps> & TNeedsProps> & {WrappedComponent: Component<P>}
): ComponentClass<Omit<P, keyof Shared<TInjectedProps, P>> & TNeedsProps> & {WrappedComponent: Component<P>}
}
// Injects props and removes them from the prop requirements.

View File

@@ -349,8 +349,7 @@ connect<ICounterStateProps, ICounterDispatchProps, {}, ICounterStateProps & ICou
class App extends Component<any, any> {
render(): JSX.Element {
// ...
render() {
return null;
}
}
@@ -544,14 +543,12 @@ interface TestState {
class TestComponent extends Component<TestProp & DispatchProp, TestState> { }
const WrappedTestComponent = connect()(TestComponent);
// return value of the connect()(TestComponent) is of the type TestComponent
let ATestComponent: React.ComponentClass<TestProp> = null;
ATestComponent = TestComponent;
ATestComponent = WrappedTestComponent;
let anElement: ReactElement<TestProp>;
// return value of the connect()(TestComponent) is assignable to a ComponentClass<TestProp>
// ie: DispatchProp has been removed through decoration
const ADecoratedTestComponent: React.ComponentClass<TestProp> = WrappedTestComponent;
<WrappedTestComponent property1={42} />;
<ATestComponent property1={42} />;
const ATestComponent: React.ComponentClass<TestProp> = TestComponent; // $ExpectError
class NonComponent {}
// this doesn't compile
@@ -748,7 +745,8 @@ namespace Issue15463 {
interface ISpinnerProps{
showGlobalSpinner: boolean;
}
class SpinnerClass extends React.Component<ISpinnerProps & DispatchProp, undefined> {
class SpinnerClass extends React.Component<ISpinnerProps & DispatchProp> {
render() {
return (<div />);
}
@@ -815,7 +813,7 @@ namespace TestControlledComponentWithoutDispatchProp {
}
namespace TestDispatchToPropsAsObject {
const onClick: ActionCreator<{}> = null;
const onClick: ActionCreator<{}> = () => ({});
const mapStateToProps = (state: any) => {
return {
title: state.app.title as string,
@@ -900,24 +898,71 @@ namespace TestCreateProvider {
ReactDOM.render(<Combined />, document.body);
}
namespace TestTypeInference {
interface State { a: number };
namespace TestWithoutTOwnPropsDecoratedInference {
const OnlyState = connect(
(state: {a: number}, props: {b: number}) => ({a: state.a, c: state.a + props.b})
)(props => <span>{props.a} + {props.b} = {props.c}</span>)
interface State { a: number };
ReactDOM.render(<OnlyState b={1} />, document.body);
interface ForwardedProps {
forwarded: string;
}
const OnlyDispatch = connect(
undefined,
(dispatch, props: {b: number}) => ({action: () => dispatch({type: 'action', b: props.b})})
)(props => <span onClick={props.action}>{props.b}</span>)
ReactDOM.render(<OnlyDispatch b={1} />, document.body);
interface OwnProps {
own: string;
}
const StateAndDispatch = connect(
(state: {a: number}, props: {b: number}) => ({a: state.a, c: state.a + props.b}),
(dispatch, props: {b: number}) => ({action: () => dispatch({type: 'action', b: props.b})})
)(props => <span>{props.a} + {props.b} = {props.c}</span>)
ReactDOM.render(<StateAndDispatch b={1} />, document.body);
interface StateProps {
state: string;
}
class WithoutOwnPropsComponentClass extends React.Component<ForwardedProps & StateProps & DispatchProp<any>> {
render() {
return <div />;
}
}
const WithoutOwnPropsComponentStateless: React.StatelessComponent<ForwardedProps & StateProps & DispatchProp<any>> = () => (<div />);
function mapStateToProps4(state: any, ownProps: OwnProps): StateProps {
return { state: 'string' };
}
// these decorations should compile, it is perfectly acceptable to receive props and ignore them
const ConnectedWithOwnPropsClass = connect(mapStateToProps4)(WithoutOwnPropsComponentClass);
const ConnectedWithOwnPropsStateless = connect(mapStateToProps4)(WithoutOwnPropsComponentStateless);
const ConnectedWithTypeHintClass = connect<StateProps, void, OwnProps>(mapStateToProps4)(WithoutOwnPropsComponentClass);
const ConnectedWithTypeHintStateless = connect<StateProps, void, OwnProps>(mapStateToProps4)(WithoutOwnPropsComponentStateless);
// This should compile
React.createElement(ConnectedWithOwnPropsClass, { own: 'string', forwarded: 'string' });
React.createElement(ConnectedWithOwnPropsClass, { own: 'string', forwarded: 'string' });
// This should not compile, it is missing ForwardedProps
React.createElement(ConnectedWithOwnPropsClass, { own: 'string' }); // $ExpectError
React.createElement(ConnectedWithOwnPropsStateless, { own: 'string' }); // $ExpectError
// This should compile
React.createElement(ConnectedWithOwnPropsClass, { own: 'string', forwarded: 'string' });
React.createElement(ConnectedWithOwnPropsStateless, { own: 'string', forwarded: 'string' });
// This should not compile, it is missing ForwardedProps
React.createElement(ConnectedWithTypeHintClass, { own: 'string' }); // $ExpectError
React.createElement(ConnectedWithTypeHintStateless, { own: 'string' }); // $ExpectError
interface AllProps {
own: string
state: string
}
class AllPropsComponent extends React.Component<AllProps & DispatchProp<any>> {
render() {
return <div />;
}
}
type PickedOwnProps = Pick<AllProps, "own">
type PickedStateProps = Pick<AllProps, "state">
const mapStateToPropsForPicked: MapStateToProps<PickedStateProps, PickedOwnProps, {}> = (state: any): PickedStateProps => {
return { state: "string" }
}
const ConnectedWithPickedOwnProps = connect(mapStateToPropsForPicked)(AllPropsComponent);
<ConnectedWithPickedOwnProps own="blah" />
}

View File

@@ -11,7 +11,7 @@
],
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": false,
"strictNullChecks": true,
"strictFunctionTypes": true,
"baseUrl": "../",
"jsx": "react",

View File

@@ -4,7 +4,7 @@
// Shoya Tanaka <https://github.com/8398a7>
// Mykolas <https://github.com/mykolas>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import {
Store,

View File

@@ -2,7 +2,7 @@
// Project: https://github.com/mjrussell/redux-auth-wrapper
// Definitions by: Karol Janyst <https://github.com/LKay>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import { ComponentClass, StatelessComponent, ComponentType, ReactType } from "react";

View File

@@ -2,7 +2,7 @@
// Project: https://github.com/gaearon/redux-devtools
// Definitions by: Petryshyn Sergii <https://github.com/mc-petry>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import * as React from 'react';
import { GenericStoreEnhancer } from 'redux';

View File

@@ -2,7 +2,7 @@
// Project: https://github.com/erikras/redux-form
// Definitions by: Carson Full <https://github.com/carsonf>, Daniel Lytkin <https://github.com/aikoven>, Karol Janyst <https://github.com/LKay>, Luka Zakrajsek <https://github.com/bancek>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import {
ComponentClass,

View File

@@ -127,7 +127,7 @@ const ConnectedDecoratedInitializeFromStateFormFunction = connect(
// React ComponentClass instead of StatelessComponent
class InitializeFromStateFormClass extends React.Component<Props & DispatchProp<any>> {
class InitializeFromStateFormClass extends React.Component<Props> {
render() {
return InitializeFromStateFormFunction(this.props);
}

View File

@@ -2,7 +2,7 @@
// Project: https://github.com/FormidableLabs/redux-little-router
// Definitions by: priecint <https://github.com/priecint>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.6
// TypeScript Version: 2.8
import * as React from "react";
import { Reducer, Middleware, StoreEnhancer } from "redux";