@types/xxxx
package and had problems.Definitions by:
in index.d.ts
) so they can respond.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,
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.
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.
@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.
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 thatMyComponent
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 errorconnect<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.