Definitelytyped: Some existing connect calls are broken by 5.0.16

Created on 11 Apr 2018  ·  3Comments  ·  Source: DefinitelyTyped/DefinitelyTyped

  • [x] I tried using the @types/xxxx package and had problems.
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
  • [x] I have a question that is inappropriate for StackOverflow. (Please ask any appropriate questions there).
  • [x] [Mention](https://github.com/blog/821-mention-somebody-they-re-notified) the authors (see Definitions by: in index.d.ts) so they can respond.

    • Authors: @Pajn @tkqubo @thasner @kenzierocks @clayne11 @tansongyang @NicholasBoll @mDibyo @pdeva

The Issue(s)

Hello! I've just updated to 5.0.16 and immediately encountered some trouble using these new typings in my existing project. Here are some relevant versions numbers:

@types/react-redux: ^5.0.16,
typescript: ^2.8.x,
react-redux: ^5.0.5,

A Typical Container does not Compile

Here is some code that I think should compile (and which did compile prior to 5.0.16 and which compiles again if we set the version back to 5.0.15):

// components/exampleComponent.ts
import React = require('react')
import { IState, IDateRange, ReportType } from '../../types/index'

export interface IExampleProps {
  label?: string
  dateRange: IDateRange
  onDateRangeChange: (value: IDateRange) => void
}

export class ExampleComponent extends React.Component<IExampleProps> {
  // implementation
}
// containers/exampleComponent.ts
import { connect } from 'react-redux'
import { IState, IDateRange, ReportType } from '../../types/index'

interface IPropsFromState {
  dateRange: IDateRange
}

interface IPropsFromParent {
  reportType: ReportType
}

const mapStateToProps = (state: IState, props: IPropsFromParent): IPropsFromState => {
  const config = state.reporting.reportConfigs[props.reportType]

  return {
    dateRange: config ? config.dateRange : null,
  }
}

const connector = connect<IPropsFromState, null, IPropsFromParent>(
  mapStateToProps,
)

export const ExampleContainer = connector(ExampleComponent) // <-- this does not compile

Normally I would go on to use the container like this,

<ExampleContainer reportType={ReportType.USERS_CREATED} />

However, with this code, using the latest @types/react-redux at 5.0.16 and typescript at 2.8.1 I instead get the following error:

[ts]
Argument of type 'typeof ExampleComponent' is not assignable to parameter of type 'ComponentType<IPropsFromState & DispatchProp<any> & IPropsFromParent>'.
  Type 'typeof ExampleComponent' is not assignable to type 'StatelessComponent<IPropsFromState & DispatchProp<any> & IPropsFromParent>'.
    Type 'typeof ExampleComponent' provides no match for the signature '(props: IPropsFromState & DispatchProp<any> & IPropsFromParent & { children?: ReactNode; }, context?: any): ReactElement<any>'.

Inspecting the changes introduced in 5.0.16, it feels like the type system is upset because connector has type InferableComponentEnhancerWithProps<IPropsFromState & IPropsFromParent, IPropsFromParent> which means that it is expecting the first argument to be of type IPropsFromState & IPropsFromParent. My base component is missing the property reportType.

For example, if I write this:

const f = <P extends IPropsFromParent & IPropsFromState>(component: Component<P>) => { /**/ }
const g = <P extends IPropsFromParent & IPropsFromState>(props: P) => { /**/ }

const record: IExampleProps = null

f(ExampleComponent)
g(record)

These minimal examples fail. The first fails for the same reason as the code above, with the same opaque error message. The second fails because, clearly, IExampleProps does not have a property reportType: ReportType so we can't assign IExampleProps to P. It is missing reportType. I suspect this is, under the hood, what is causing the first example to fail.

It seems like after 5.0.16 containers are not allowed to add new properties to the interfaces of their wrapped components? This is what I see, looking at the code.

It is part of my normal workflow to define components in terms of only what properties they need, and then to extend those components with HOCs that define a new interface and provide the component its properties via the redux store. In some cases the new interface is a subset of the wrapped component's interface, but in other cases it may not be. In the case above, the new interface has an additional property reportType which the caller will use but that the wrapped component will never see.


A More Idiosyncratic Container does not Compile

I have an another example of a technique I use that is broken by this change:

Suppose we have two simple components,

class NameDateComponent extends React.PureComponent<{ name: string, date: string}> {}

class DateOnlyComponent extends React.PureComponent<{ date: string }> {}

I want to build an HOC that provides the date for these components without needing to know anything about the name property at all. This way I can make, for example, a NameProvider and a DateProvider, and compose them like this: NameProvider(DateProvider(BaseComponent)) or like this DateProvider(NameProvider(BaseComponent)), something which I normally can't do with a well typed component enhancer because of the need to specify TOwnProps.

interface IWithDate {
  date: string
}

// ComponenProps defines a component interface that includes IWithDate
// NewAPI defines a new interface that excludes IWithDate
// so the types represent only what will change in the given component, without needing to know the rest of the interface
function DateProvider <ComponentProps extends IWithDate, NewAPI extends Minus<ComponentProps, IWithDate>> (Base: CompositeComponent<ComponentProps>) {

  const enhancer = connect<ComponentProps, null, NewAPI>(
    (_state: IState, props: NewAPI): ComponentProps => {
      // because of the 'never' type, 
      // typescript doesn't think NewAPI & IWithProps == ComponentProps
      // so we have to coerce this (but you and I can see that the above holds)
      const newProps: any = Object.assign({
        date: '2017-01-01'
      }, props)

      return newProps as ComponentProps
    },
    null
  )

  return enhancer(Base) // <-- after 5.0.16 this line does not compile
}

This new interface agnostic component enhancer is used as follows:

const NameOnly = DateProvider(NameDateComponent)
const Nothing = DateProvider(DateOnlyComponent)

.
.
.

<Nothing />
<NameOnly name='Bob' />
<NameDateComponent name='Bob' date='2017-01-01' />

So the DateProvider HOC is able to augment a component without being fully aware of that component's interface. It only needs to know that the given component will be capable of accepting the props that it provides.

With 5.0.16 this no longer works, instead I get the following error message:

[ts]
Argument of type 'CompositeComponent<ComponentProps>' is not assignable to parameter of type 'ComponentType<ComponentProps & NewAPI>'.
  Type 'ComponentClass<ComponentProps>' is not assignable to type 'ComponentType<ComponentProps & NewAPI>'.
    Type 'ComponentClass<ComponentProps>' is not assignable to type 'StatelessComponent<ComponentProps & NewAPI>'.
      Types of property 'propTypes' are incompatible.
        Type 'ValidationMap<ComponentProps>' is not assignable to type 'ValidationMap<ComponentProps & NewAPI>'.
          Type 'keyof ComponentProps | keyof NewAPI' is not assignable to type 'keyof ComponentProps'.
            Type 'keyof NewAPI' is not assignable to type 'keyof ComponentProps'.
              Type 'keyof NewAPI' is not assignable to type '"date"'.

Playing around, I've noticed the following:

interface IWithDate {
  date: string
}

interface IComponentProps {
  name: string
  date: string
}

// I've torn out the internals of the DateProvider above
const connect1 = <T extends IWithDate, U extends Omit<T, keyof IWithDate>>(base: CompositeComponent<T>) => {
  const enhancer: InferableComponentEnhancerWithProps<T & U, U> = () => null

  return enhancer(base) // <-- this is a compile error
}

const connect2 = <T extends IWithDate, U extends Omit<T, keyof IWithDate>>() => {
  const enhancer: InferableComponentEnhancerWithProps<T & U, U> = () => null

  return enhancer
}

const enhancer = connect2<IComponentProps, Omit<IComponentProps, keyof IWithDate>>()
const container = enhancer(NameDateComponent) // <-- this is not a compile error

When we explicitly type the connector, everything works?


That's it! Thanks for reading ;) for now I have set my @types/react-redux to a previous version and all is well.

Most helpful comment

Reverting to 5.0.15 does not completely solve the issue. While 5.0.15 will compile this kind of code without warning- it still errors if you add type-checking of the props for the presentational component in the connect call. (Assuming your wrapping component has a prop that MyComponent does not)

For example this will work in 5.0.15, but not 5.0.16
connect(stateToProps, dispatchToProps)(MyComponent)

However- even in 5.0.15, you can't typecheck the MyComponent props without compiler error
connect<statetoprops, dispatchtoprops, mycomponentprops)(stateToProps, dispatchToProps)(MyComponent)
This will error complaining that the wrapping components prop doesn't exist in MyComponent

I think this is a larger issue that has been lurking in the type mappings that @Pajn 's cleanup brought to light.

All 3 comments

@variousauthors could you please share the versions of @types/react and @types/react-redux you are using ?

EDIT: the @ messed up things ans the names of the libs did not appear, sorry :disappointed:

@variousauthors I think you're spot on with this comment

It seems like after 5.0.16 containers are not allowed to add new properties to the interfaces of their wrapped components? This is what I see, looking at the code.

The bug is that the type definitions for redux's connect function intersects ownProps and mapPropsToState. This means that any "container" component is forced to have its props replicated in any derived "presentational" components- which breaks common redux design patterns.

PR that introduced: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24764
Other issue: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/24922

Reverting to 5.0.15 does not completely solve the issue. While 5.0.15 will compile this kind of code without warning- it still errors if you add type-checking of the props for the presentational component in the connect call. (Assuming your wrapping component has a prop that MyComponent does not)

For example this will work in 5.0.15, but not 5.0.16
connect(stateToProps, dispatchToProps)(MyComponent)

However- even in 5.0.15, you can't typecheck the MyComponent props without compiler error
connect<statetoprops, dispatchtoprops, mycomponentprops)(stateToProps, dispatchToProps)(MyComponent)
This will error complaining that the wrapping components prop doesn't exist in MyComponent

I think this is a larger issue that has been lurking in the type mappings that @Pajn 's cleanup brought to light.

Was this page helpful?
0 / 5 - 0 ratings