@types/xxxx
package and had problems.Definitions by:
in index.d.ts
) so they can respond.Mentioning @blakeembrey, @andy-ms, @alecmerdler 馃槈
Code:
import React, { Component } from "react";
import { connect } from "react-redux";
import { RootState } from "@src/state/state";
interface Props {
normal: string;
optional?: number;
}
class TestComponent extends Component<Props> {
render() {
return <h1>Hello</h1>;
};
}
function mapStateToProps(_state: RootState) {
return {
normal: "test",
optional: 5,
};
}
const Connected = connect(mapStateToProps)(TestComponent);
Error:
[ts]
Argument of type 'typeof TestComponent' is not assignable to parameter of type 'ComponentType<{ normal: string; optional: number | undefined; } & DispatchProp<any>>'.
Type 'typeof TestComponent' is not assignable to type 'StatelessComponent<{ normal: string; optional: number | undefined; } & DispatchProp<any>>'.
Type 'typeof TestComponent' provides no match for the signature '(props: { normal: string; optional: number | undefined; } & DispatchProp<any> & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.
The problem is when the optional
prop is marked as optional - the TestComponent
part of connect(mapStateToProps)(TestComponent);
is reported red with above error code.
For unknown reasons the inferred type is StatelessComponent
but TestComponent
is a class.
When the prop is marked as optional: number|undefined
all is ok, so it's a workaround for now.
The workaround doesn't work when we use 3rd party HOC, like redux-form, which has some props optional, like initialValues?: Partial<FormData>;
:
Code to reproduce:
interface State {
stateString: string;
}
interface CompProps {
propString: string;
}
interface FormData {
name: string;
age: number;
}
class TestComponent extends Component<CompProps& InjectedFormProps<FormData, CompProps>> {}
export const Form = reduxForm<FormData, CompProps>({
form: "test",
destroyOnUnmount: true,
})(TestComponent);
export const Connected = connect(
(state: State) => ({
propString: state.stateString,
initialValues: { name: "", age: 23 },
}),
)(Form);
I got this not meaningful error:
Argument of type 'DecoratedComponentClass<FormData, CompProps & Partial<ConfigProps<FormData, CompProps>>>' is not assignable to parameter of type 'ComponentType<{ propString: string; initialValues: { name: string; age: number; }; } & DispatchPr...'.
Type 'DecoratedComponentClass<FormData, CompProps & Partial<ConfigProps<FormData, CompProps>>>' is not assignable to type 'StatelessComponent<{ propString: string; initialValues: { name: string; age: number; }; } & Dispa...'.
Type 'DecoratedComponentClass<FormData, CompProps & Partial<ConfigProps<FormData, CompProps>>>' provides no match for the signature '(props: { propString: string; initialValues: { name: string; age: number; }; } & DispatchProp<any> & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.
In connect I can specify only non optional and not undefined props, like "form". Even marking as required and with undefined union doesn't work in this case.
The pick props from redux-form config works ok, if I doesn't provide form
name in redux-form, I had to to this in connect or in JSX <Form form="test" />
.
Ping authors after 2 weeks 馃槈
@tkqubo @thasner @kenzierocks @clayne11 @tansongyang @NicholasBoll @mDibyo
Looks like we should update the Diff
and Omit
types to the new ones?
https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-319495340
I believe this may have been fixed by https://github.com/DefinitelyTyped/DefinitelyTyped/pull/21730. I needed to make additional changes in my project to account for https://github.com/DefinitelyTyped/DefinitelyTyped/pull/21400 but after upgrading to v5.0.14 everything seems happy again.
@rafeememon
Unfortunately not 馃槥 I've installed 5.0.14
and the error is still here:
The problem is that connect
types perform the type check from inverse side, so optional type a?: A
is not assignable to type a: A|undefined
.
And because of that, when your props are a: A
, you can assign maybe undefined value to the a
field and have no error at all, like photo: 1 !== 1 ? state.photos[0] : undefined
馃檧
Mentioning all authors for any help: @tkqubo, @thasner, @kenzierocks, @clayne11, @tansongyang, @nicholasboll, @mdibyo, @pdeva
There still seems to be issues with optional props as described by @19majkel94 one comment above. I am seeing similar behaviour and was able to reproduce this in a simple test case:
namespace MapStateOptionalProp {
interface Props {
bar?: number,
test: boolean
}
const TestComponent: React.StatelessComponent<Props> = ({bar = 1}) => null
const mapStateToProps = (_: any) => ({
bar: 1,
test: false
})
const Test = connect(
mapStateToProps
)(TestComponent)
const verify = <Test />
}
This fails with:
[ts] Argument of type 'StatelessComponent<Props>' is not assignable to parameter of type 'ComponentType<{ bar: number; test: boolean; } & DispatchProp<any>>'.
on types/react-redux 5.0.14 and typescript 2.6.2. Any suggestions what might be the issue?
Any suggestions what might be the issue?
Yes, the problem is with HOC - they behave like function that accept an argument (a component). So connect
infer the props
type from mapStateToProps
function and the compiler check if the component props match the connect props. So it failed because bar
is number
so Props
with optional bar
is not assignable.
The solution is to have interfaces for mapStateToProps
props type to disable the inferring of not optional type.
Having the same issue here :/
Fun fact: it works normally with functional components, but not with class components.
I hit this. You can fix easily enough with a cast.
function mapStateToProps(_state: RootState) {
return {
normal: "test",
optional: 5,
} as Partial<Props>;
}
At least it works for me.
@DanHarman Partial<Props>
- Props coming from where?
@nwinger My suggestion was if you had code as per the original in the first post.
@19majkel94 Could you try this? (Adapted the example from the initial post in this issue)
Only the last line has changed.
import React, { Component } from "react";
import { connect } from "react-redux";
import { RootState } from "@src/state/state";
interface Props {
normal: string;
optional?: number;
}
class TestComponent extends Component<Props> {
render() {
return <h1>Hello</h1>;
};
}
function mapStateToProps(_state: RootState) {
return {
normal: "test",
optional: 5,
};
}
const Connected = connect<Props>(mapStateToProps)(TestComponent);
What about mapDispatchToProps
and OwnProps
? I had to create 3 interfaces and the union type to merge them together in Props
. I know it works, the issue is about inferencing from shape of connect parameters and it can't be done for now as it's how TS works with HOC 馃槙
@19majkel94 Just read the rest of the discussion above thoroughly. Sorry for bringing up something that has already been discussed.
And I understand your concern with having to define the extra interfaces/types. Quite often, more than half of my component file is spent on that boilerplate.
@DanHarman
Casting to Partial<Props>
can be error-prone. If you forget to provide a required prop, typescript won't report such error.
It works fine if we cast it to a stateless component.
connect(mapStateToProps, mapDispatch)(MyComponent as any as React.SFC<Props>)
Or we can use conditional types to create an util for converting to SFC. It's a better solution if we don't have Props
definition.
type ToSFC<T> = T extends React.ComponentClass<infer Props> ? React.SFC<Props> : T;
function toSFC<T>(component: T) {
return component as ToSFC<T>;
}
connect(mapStateToProps, mapDispatch)(toSFC(MyComponent))
I had this issue. The problem was the return type of my mapStateToProps
functions didn't have the same optionals the original Props
type did. I fixed it by using Pick
in the following way, pulled from my code based on the Typescript React tutorial:
export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
let props = {
enthusiasmLevel,
name: languageName
}
return props as Pick<Props, keyof typeof props> // this is the key line
}
Here's my full example:
interface Props {
name: string // this comes from redux state
enthusiasmLevel?: number // this comes from redux state
onIncrement?: () => any // this comes from redux dispatch
onDecrement?: () => any // this comes from redux dispatch
greeting: string // this is passed through the container component
}
class Hello extends React.Component<Props> {
render() {
return ( /* snipped for brevity */ )
}
}
function mapStateToProps({ enthusiasmLevel, languageName }: MyState) {
let props = {
enthusiasmLevel,
name: languageName
}
return props as Pick<Props, keyof typeof props>
}
function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
let props = {
onIncrement: () => dispatch(actions.incrementEnthusiasm()),
onDecrement: () => dispatch(actions.decrementEnthusiasm())
}
return props as Pick<Props, keyof typeof props>
}
export default connect(mapStateToProps, mapDispatchToProps)(Hello)
After sometime of search, this is the best solution doc of TypeScript + React:
https://github.com/piotrwitek/react-redux-typescript-guide
Most of scenarios with solution will be found here. And for my use, I use Material UI, I'm glad to share my demos below.
import * as React from 'react'
import { formatSex, formatDocState, formatGoal, formatPriority } from '@app/state/ducks/crm/doc/types'
import TableRow from '@material-ui/core/TableRow'
import TableCell from '@material-ui/core/TableCell'
import Hidden from '@material-ui/core/Hidden'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Theme, withStyles, WithStyles } from '@material-ui/core/styles'
import config from '@app/../config'
const V = config.VERSION
const styles = (theme: Theme) => ({
row: {
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.background.default,
},
},
})
export interface DocListItemViewProps extends RouteComponentProps<{}> {
id: number
name: string
sex: number
priority: number
dealPossibility: number
mobile: string
docState: number
goal: number
}
type PropsWithStyles = DocListItemViewProps & WithStyles<'row'>
const DocListItem: React.SFC<PropsWithStyles> = (props) => {
const { id, name, priority, dealPossibility, mobile, sex, docState, goal, classes } = props
return (
<TableRow
onClick={() => {
props.history.push(`/${V}/crm/doc/updateDoc/${id}`)
}}
className={classes.row}
>
<Hidden mdDown={true}>
<TableCell>{id}</TableCell>
</Hidden>
<TableCell>{name}</TableCell>
<Hidden xsDown={true}>
<TableCell>{formatSex(sex)}</TableCell>
</Hidden>
<TableCell>{formatPriority(priority)}</TableCell>
<Hidden xsDown={true}>
<TableCell>{formatDocState(docState)}</TableCell>
</Hidden>
<Hidden smDown={true}>
<TableCell>{dealPossibility}</TableCell>
</Hidden>
<Hidden lgDown={true}>
<TableCell>{formatGoal(goal)}%</TableCell>
</Hidden>
<TableCell>{mobile}</TableCell>
</TableRow>
)
}
export default withRouter(withStyles(styles)(DocListItem))
import * as React from 'react'
import CRMLayout from '@app/views/layout/CRMLayout'
import { withStyles, WithStyles } from '@material-ui/core/styles'
import { hot } from 'react-hot-loader'
import { connect } from 'react-redux'
import { RootState } from 'src/app/state/rootReducer'
import * as SharedActions from '@app/state/ducks/shared/actions'
import { BOTTOM_NAVIGATION_ITEM_TYPE } from '@app/state/ducks/shared/types'
import Avatar from '@material-ui/core/Avatar'
import Paper from '@material-ui/core/Paper'
import * as classnames from 'classnames'
import DataSummaryItem from '@app/views/components/common/DataSummaryItem'
import NameSummaryItem from '@app/views/components/common/NameSummaryItem'
import ScheduleList from '@app/views/components/common/ScheduleList'
import ActivityList from '@app/views/components/common/ActivityList'
const decorator = withStyles(({ spacing }) => ({
container: {
},
block: {
marginBottom: spacing.unit,
padding: spacing.unit * 2,
},
summary: {
display: 'flex',
flexDirection: 'row' as 'row',
},
schedules: {
},
activities: {
},
avatar: {
width: 60,
height: 60,
},
}))
interface CRMIndexProps {
switchBottomNavigation: (item: BOTTOM_NAVIGATION_ITEM_TYPE) => any
}
interface CRMIndexStates {
}
type PropsWithStyles = CRMIndexProps & WithStyles<'container' | 'block' | 'summary' | 'schedules' | 'activities' | 'avatar'>
const CRMIndex = decorator(
class extends React.Component<PropsWithStyles, CRMIndexStates> {
componentDidMount() {
this.props.switchBottomNavigation(BOTTOM_NAVIGATION_ITEM_TYPE.CRM_INDEX)
}
render() {
const { classes } = this.props
return (
<CRMLayout>
<div className={classes.container}>
<Paper square={true} className={classnames(classes.summary, classes.block)}>
<Avatar
src="https://cdn.bolifestudio.com/public/uploads/1_2_avatar_1522051846699?x-oss-process=image/resize,w_128"
className={classes.avatar}
/>
<NameSummaryItem name="Bosn" title="Test" />
<DataSummaryItem label="aaa" value="ccc" />
<DataSummaryItem label="bbb" value="ddd" />
</Paper>
<Paper square={true} className={classnames(classes.schedules, classes.block)}>
<ScheduleList />
</Paper>
<Paper square={true} className={classnames(classes.activities, classes.block)}>
<ActivityList />
</Paper>
</div>
</CRMLayout>
)
}
}
)
const mapStateToProps = (_state: RootState) => {
return {
}
}
const mapDispatchToProps = ({
switchBottomNavigation: SharedActions.switchBottomNavigation,
})
export default connect(mapStateToProps, mapDispatchToProps)(hot(module)(CRMIndex))
@Bosn, if I'm reading your examples correctly, I believe your second example would not compile if switchBottomNavigation
was optional in interface CRMIndexProps
, which is the issue in this thread. @BetterCallSky's React.SFC
example solves that issue, but then causes errors with pass through props (ones that are not mapped from state or dispatch). I'll have to work with it a bit longer to be 100% sure, but I believe that using Pick
in the mapping functions (as in my example above) cleanly solves both issues while maintaining type inference in connect
and minimizing code duplication.
@sargunv I think this issue had been fixed, I use the newest version packages, and try to rebuild the error, but it compiles successfully. My code:
import * as React from 'react'
import { connect } from 'react-redux'
import { RootState } from 'src/app/state/rootReducer'
export interface DocListViewProps {
optional?: number
}
export interface DocListViewStates {
}
class SalesAnalyze extends React.Component<DocListViewProps, DocListViewStates> {
render() {
const { optional } = this.props
return (
<div>
sales charts page{optional}
</div>
)
}
}
const mapStateToProps = (_state: RootState) => {
return {
}
}
const mapDispatchToProps = ({
})
export default connect(mapStateToProps, mapDispatchToProps)(SalesAnalyze)
@Bosn You need to populate the optional prop in mapStatesToProps
to reproduce the error.
@sargunv The example given by the issue author can be compiled successfully via TypeScript 2.9.0 (Using VSCode Insider, at right/bottom corner, choose 2.9.0-insiders.20180510
For older versions, this can be fixed by just adding function returning type of mapStateToProps
as Props
, this is the full example.
import * as React from 'react'
import { connect } from 'react-redux'
interface RootState {
}
interface Props {
normal: string
optional?: number
}
class TestComponent extends React.Component<Props> {
render() {
return <h1>Hello</h1>
}
}
function mapStateToProps(_state: RootState): Props { // here, I add the type to match, or use new derived interface.
return {
normal: 'test',
optional: 5,
}
}
const Connected = connect(mapStateToProps)(TestComponent)
export default Connected
For convenience, also can use Partial<Props>
as the returning type of mapStateToProps
. For strictly use, I think it'd be better to define by yourself to match the interface below.
interface MapProps {
normal: string
optional?: number
}
function mapStateToProps(_state: RootState): MapProps {
return {
normal: 'test',
optional: 5,
}
}
@Bosn That's exactly what the Pick
solution is doing, except without duplicating the prop definitions.
These two code blocks are essentially equivalent, but the second avoids defining the normal
and optional
props twice.
interface Props {
normal: string
optional?: number
other: string
}
interface MapProps {
normal: string
optional?: number
}
function mapStateToProps(_state: RootState): MapProps {
return {
normal: 'test',
optional: 5,
}
}
interface Props {
normal: string
optional?: number
other: string
}
function mapStateToProps(_state: RootState) {
let props = {
normal: 'test',
optional: 5,
}
return props as Pick<Props, keyof typeof props>
}
@sargunv Your solution is shorter, but can't be right. You can see my example, when property normal
is REQUIRED, for your method, all properties will be OPTIONAL.
import * as React from 'react'
import { connect } from 'react-redux'
interface RootState {
}
interface Props {
normal: string
optional?: number
other: string
}
class TestComponent extends React.Component<Props> {
render() {
return <h1>Hello</h1>
}
}
function mapStateToProps(_state: RootState) {
const props = {
other: 'haha', // property normal required !!! But no compile error~
}
return props as Pick<Props, keyof typeof props>
}
const Connected = connect(mapStateToProps)(TestComponent)
export default Connected
Posting this here for feedback to this issue...
I hate declaring all Redux props as optional, so I opt to create two interfaces; Props & ReduxProps. That way when I implement the component, I don't get any extraneous properties showing (the optional redux-connect props). Problem is, when I use connect() I get a ts error that says
Argument of type 'typeof TestComponent' is not assignable to parameter of type 'ComponentType<never>'.
Type 'typeof TestComponent' is not assignable to type 'ComponentClass<never, any>'.
Types of property 'defaultProps' are incompatible.
Type 'Partial<Props>' is not assignable to type 'never'.
In order to fix that issue and get sane code completion, I have resorted to casting the components. Here's what I've done, written as the TestComponent:
import * as React from 'react'
import { connect } from 'react-redux'
interface Props {
normal: string
optional?: number
other: string
}
interface ReduxProps {
isAuthenticated: boolean;
username: string;
}
class TestComponent extends React.Component<Props & ReduxProps> {
constructor(props: Props & ReduxProps) {
super(props);
}
render() {
return <h1>Hello{this.props.isAuthenticated ? ` ${this.props.username}` : ''}</h1>
}
}
const mapState = state => ({
isAuthenticated: state.auth.isAuthenticated,
username: state.auth.username,
});
// Casting to prevent TS from throwing error output as above
const Connected = connect(
mapStateToProps
)(
TestComponent as React.ComponentClass<Props & ReduxProps> // cast the connect-input component with all props
) as React.ComponentClass<Props> // cast the final component just with Props
export default Connected
Now code completion will only show normal, other, optional?
I hate having to do the extra casting so if you have suggestions on why this is necessary please fire away.
Any clue how to fix this at the source?
In index.d.ts I'll have a look around the Omit
of InferableComponentEnhancerWithProps
...
Any hint is welcome :')
This is still an issue, a year later. Is there anyone that can identify and fix this issue?
@DylanVann I saw that you commited recently on this project, would you mind to take a look on this, or redirect to someone who can? Thx
I'd try using the hooks API instead of using connect
. Typing HOCs is too painful.
@DylanVann I far as I can see, hooks api works for function components, we are using class components.
For all participants in this thread, if you use yarn
(I'm not sure about npm
), please refer the typings version on resolutions
node of package.json
.
These are my versions:
"resolutions": {
"@types/react": "16.9.11",
"@types/prop-types": "15.7.3",
"@types/react-dom": "16.9.4",
"@types/react-redux": "7.1.5"
},
After this change some incompatible type errors are gone.
Most helpful comment
I hit this. You can fix easily enough with a cast.
At least it works for me.