Can't handle typescript with react and redux - reactjs

I am new at typescript and really struggling. I can't understand where to begin and where to end . Yes , there are many recourses on internet , but i couldn't manage to get that information and use in my project . Hope to meet some help here. I have also done some typechecking and if you find something that i could have done better , please just tell me to improve it.
So now i am struggling with redux mapStateToProps and mapDispatchToProps . I have tried many variants , but every time i have got some kind of errors.
I will post my code which represents my dashboard component which is connected to state .
import * as React from 'react';
import { connect } from 'react-redux';
import SearchIcon from '../SvgIcons';
import MapComponent from '../Map';
import { getEventInfo, getUserInfo } from '../../actions';
interface StateProps {
userReducer: {
accessToken: string
},
eventReducer: {
events: object[]
}
}
interface DispatchProps {
dispatchUserInfo: () => void;
dispatchEventInfo: (accessToken: string, query: string) => void;
}
interface OwnProps {
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void,
accessToken: string,
events: object[]
}
type Props = StateProps & DispatchProps & OwnProps;
class DashboardPage extends React.Component<Props, {}> {
componentDidMount() {
const { dispatchUserInfo } = this.props;
dispatchUserInfo();
}
handleEventSearch = e => {
e.preventDefault();
const { dispatchEventInfo, accessToken } = this.props;
const query: string = e.target.children[0].value;
dispatchEventInfo(accessToken, query);
}
render() {
const { events } = this.props;
return (
<div className="dashboard-container">
<div className="search-event">
<form className="search-event__form" onSubmit={this.handleEventSearch}>
<input
autoComplete="off"
type="text"
name="search-event"
placeholder="Search an event"
className="search-event__input"
aria-label="Enter search text"
/>
<button type="submit" className="search-event__button">
<SearchIcon />
Search
</button>
</form>
<p className="sign-out">
Sign out
</p>
</div>
<div className="google-map">
<MapComponent events={events} />
</div>
</div>
);
}
}
const mapStateToProps = (state: StateProps) => {
const accessToken = state.userReducer.accessToken || '';
const events = state.eventReducer || [];
return {
accessToken,
events
};
};
const mapDispatchToProps = (dispatch: DispatchProps) => ({
dispatchEventInfo(query: string, token: string) {
dispatch(getEventInfo(query, token));
},
dispatchUserInfo() {
dispatch(getUserInfo());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage);
These are typescript errors
1) Refers tohandleEventSearch`method
[ts] Parameter 'e' implicitly has an 'any' type.
2) refers to mapDispatchToProps
[ts] Cannot invoke an expression whose type lacks a call signature. Type 'DispatchProps' has no compatible call signatures.
3) refers to mapDispatchToProps in connect HOC
Argument of type '(dispatch: DispatchProps) => { dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }' is not assignable to parameter of type 'MapDispatchToPropsParam<{ dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }, {}>'.
Type '(dispatch: DispatchProps) => { dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }' is not assignable to type 'MapDispatchToPropsFunction<{ dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }, {}>'.
Types of parameters 'dispatch' and 'dispatch' are incompatible.
Type 'Dispatch>' is not assignable to type 'DispatchProps'.
Property 'dispatchUserInfo' is missing in type 'Dispatch>'.
Also if you can provide me very good sources to learn about react and redux with typescript , so i could easily write my code.

You need to supply event type explicitly, any or
React.SyntheticEvent<...something>
Dispatch parameter should be Dispatch type from Redux
Also a little tip, you can define your props like:
ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps> & OwnProps
and remove unnecessary interfaces

Related

Account for two different function variations in Props

I'm unsure how to properly type a function passed as props.
It can basically have two shapes:
// Hoc1.tsx
import ChildComponent from "./ChildComponent";
const Hoc1 = () => {
const cancelResponse = async (agreement: boolean) => {
return mockCall(agreement);
};
return <ChildComponent cancelResponse={cancelResponse} />;
};
export default Hoc1;
and
// Hoc2.tsx
import ChildComponent from "./ChildComponent";
const Hoc2 = () => {
const cancelResponse = async (data: {
agreement: boolean;
shiftId: string;
userId: string;
}) => {
return mockCall(data);
};
return (
<ChildComponent
cancelResponse={cancelResponse}
cancelData={{ shiftId: "1", userId: "2" }}
/>
);
};
export default Hoc2;
Now I'm unsure, how I should go about properly typing this. I tried using a conditional
These two pass different functions to the said ChildComponent
type CancelResponseVariant<ArgumentVariant> = ArgumentVariant extends boolean
? boolean
: { agreement: boolean; shiftId: string; userId: string };
type Props = {
cancelResponse: <T>(arg: CancelResponseVariant<T>) => Promise<void>;
cancelData?: { shiftId: string; userId: string };
};
const ChildComponent = (props: Props) => {
const { cancelData, cancelResponse } = props;
const onCancelResponse = async (agreement: boolean) => {
if (cancelData) {
await cancelResponse({ agreement, ...cancelData });
} else {
await cancelResponse(agreement);
}
};
return <button onClick={async () => onCancelResponse(true)}>Example</button>;
};
export default ChildComponent;
This seems to be nearing the solution, however is not quite it. The last call produces the following error:
Argument of type 'boolean' is not assignable to parameter of type '{ agreement: boolean; shiftId: string; userId: string; }'.ts(2345)
And in the HoC passing the function produces the following error:
Type '(agreement: boolean) => Promise' is not assignable to type '(arg: CancelResponseVariant) => Promise'.
Types of parameters 'agreement' and 'arg' are incompatible.
Type 'CancelResponseVariant' is not assignable to type 'boolean'.
Type 'boolean | { agreement: boolean; shiftId: string; userId: string; }' is not assignable to type 'boolean'.
Type '{ agreement: boolean; shiftId: string; userId: string; }' is not assignable to type 'boolean'.
Any idea how I should go about properly typing this?
Note: Obviously I tried using a simple conditional union type as well
type Data = { agreement: boolean, userId: string, shiftId: string }
type Props = {
(agreement: boolean | Data) => Promise<void>
}
but that still throws an error inside the HoC
You can see the error reproduced here:
or TSPlayground
PS: I am aware this is not the ideal way to pass the functions with different arg variations, but it's a 5 year old codebase that I need to migrate to typescript, so for now, I'm just working with what I can.
Because you have cancelData in the props alongside cancelResponse, you have a discriminated union (even when the HOC doesn't provide a cancelData property), so you can define Props like this:
type Props =
// First variant
{
cancelResponse: (arg: boolean) => Promise<unknown>;
cancelData?: never;
}
|
// Second variant
{
cancelResponse: (arg: { agreement: boolean; shiftId: string; userId: string }) => Promise<unknown>;
cancelData: { shiftId: string; userId: string };
};
(I'll explain about Promise<unknown> instead of Promise<void> in a moment, you probably don't need to make that change in your real code, but I did for the example.)
We need cancelData?: never; in the first variant because without it, we don't have a fully discriminated union. But we make it optional so HOCs that don't need it don't provide it (and thus use the simple (agreement: boolean) => Promise<unknown> signature).
ChildComponent figures out which call to make based on cancelData:
const ChildComponent = (props: Props) => {
const onCancelResponse = async (agreement: boolean) => {
if (props.cancelData) {
await props.cancelResponse({ agreement, ...props.cancelData });
} else {
await props.cancelResponse(agreement);
}
};
return <button onClick={async () => onCancelResponse(true)}>Example</button>;
};
That makes both Hoc1 and Hoc2 work, and disallows this incorrect Hoc3:
// With an intentional error
const Hoc3 = () => {
const cancelResponse = async (agreement: boolean) => {
return mockCall(agreement);
};
return <ChildComponent cancelResponse={cancelResponse} cancelData={{shiftId: "1", userId: "2"}} />;
// ^−−−−− Error as desired, the provided `cancelResponse` has the wrong signature
};
Playground link
About Promise<unknown> rather than Promise<void> in the example: That's because in Hoc1 and Hoc2, your code was doing return mockCall(agreement); and mockCall didn't have a return type annotation so ends up with Promise<unknown>. You may not need that change, if the actual calls HOCs make do have the return type Promise<void>. If they do, you're good to go with your original Promise<void>, like this.

React with Typescript: Type missmatch

its a while started using React with typescript but always getting confused with types and what React element expects.
like in this scenario (Also sandbox here) I get the follwing error for Todos Component
in VSCODE with prettier and TSLint setup: Type '(props: PropsWithChildren) => Element[]' is not assignable to type 'FunctionComponent'.
in Snadbox: Type '(props: IProps) => void[]' is not assignable to type 'FunctionComponent'.
import { TodoStore } from "./stores/TodoStore";
import { observable } from "mobx";
interface IProps {
todoStore: TodoStore;
}
// red underline at `TodoComp` with mentioned error message
const TodosComp: React.FC<IProps> = props => {
const { todos } = props.todoStore;
return todos.map(todo => {
<div key={todo.id}>
<p>{todo.title}</p>
<select />
</div>;
});
};
export default inject("todoStore")(observer(TodosComp));
The Mobx store for todos is like
import { decorate, observable, runInAction, action } from 'mobx';
export interface ITodo {userId: number; id: number; title: string; completed: boolean;
}
export class TodoStore {
public todos: ITodo[];
constructor() {
this.todos = [];
this.fetchTodos();
}
public async fetchTodos() {
const response = await fetch('http://jsonplaceholder.typicode.com/todos');
const todos: ITodo[] = await response.json();
runInAction(() => {
this.todos = todos;
});
}}
decorate<TodoStore>(TodoStore, { todos: observable, fetchTodos: action });
Is the problem from Mobx and if the class itself is used as a type in the component? does mobx classes need a separate interface?
Also as a Mobx specific question is the way in sandbox correct to instantiate the store class at provider?
in general, if somebody knows a good article about React with typescript how to manage props and JSX types would be appreciated.
FunctionComponent is defined as
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement | null;
...
where ReactElement is
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
By looking at these definitions you can tell that you can't return an array from React.FC. The solution is to wrap your return value in react fragment like
return (
<>
{todos.map(todo => {
<div key={todo.id}>
<p>{todo.title}</p>
<select />
</div>;
})}
</>
);
Function should return a JSX.Element and you need to pass a div or a Fragment, this should works.
const Todos: React.FC<IProps> = (props: IProps): JSX.Element => {
const { todos } = props.todoStore;
return (<React.Fragment>
{todos.map(todo => {
<div key={todo.id}>
<p>{todo.title}</p>
<select />
</div>;
})}
</React.Fragment>);
};

React: how use component with use custom Props interface with 'react-redux' connection decorator

I'm use React with typescript and i cant use my component with 'react-redux' connection decorator. I have are component:
import * as React from 'react';
import { connect } from 'react-redux';
import State from '../../models/State';
import { showPopup } from '../../actions/popup.actions';
import IconButton from '../IconButton/IconButton.component';
import Page from '../Page/Page.component';
import Popup from '../Popup/Popup.component';
interface Props {
popups: string[];
showPopup: Function;
hidePopup: Function;
}
class AccountsPage extends React.Component {
props: Props;
constructor(props: Props) {
super(props);
}
render() {
return (
<Page>
<div className="page__header">
<IconButton icon="add" onClick={() => 1}>Добавить</IconButton>
</div>
<div className="page__content">
<button onClick={() => this.props.showPopup()}>Show popup</button>
</div>
{
this.isPopupVisible ?
<Popup onClose={() => this.props.hidePopup()}>
<div>Hi!</div>
</Popup> :
null
}
</Page>
);
}
}
export default connect(
(state: State) => ({
popups: state.popups
}),
(dispatch: Function) => ({
showPopup: (pageName: string) => dispatch(showPopup(pageName))
hidePopup: (pageName: string) => dispatch(hidePopup(pageName))
})
)(AccountsPage);
and i'm get an error:
(62,3): Argument of type 'typeof AccountsPage' is not assignable to parameter of type 'ComponentType<{ popups: string[]; } & { showPopup: (pageName: string) => any; hidePopup: (pageNam...'.
Type 'typeof AccountsPage' is not assignable to type 'StatelessComponent<{ popups: string[]; } & { showPopup: (pageName: string) => any; hidePopup: (pa...'.
Type 'typeof AccountsPage' provides no match for the signature '(props: { popups: string[]; } & { showPopup: (pageName: string) => any; hidePopup: (pageName: string) => any; } & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.
If I'm change props: Props to props: any all works fine, how can I change my Props interface for fix this error?
Here's a fully typed example:
interface DispatchProps {
hidePopup: (pageName: string) => void;
showPopup: (pageName: string) => void;
}
interface StateProps {
popups: State['popups'];
}
type Props = DispatchProps & StateProps;
class AccountsPage extends React.Component<Props> {
isPopupVisible: boolean;
constructor(props: Props) {
super(props);
}
render() {
return (
<Page>
<div className="page__header">
<IconButton icon="add" onClick={() => 1}>Добавить</IconButton>
</div>
<div className="page__content">
<button onClick={() => this.props.showPopup()}>Show popup</button>
</div>
{
this.isPopupVisible ?
<Popup onClose={() => this.props.hidePopup()}>
<div>Hi!</div>
</Popup> :
null
}
</Page>
);
}
}
export default connect(
(state: State): StateProps => ({
popups: state.popups
}),
(dispatch: Function): DispatchProps => ({
showPopup: (pageName: string) => dispatch(showPopup(pageName)),
hidePopup: (pageName: string) => dispatch(hidePopup(pageName))
})
)(AccountsPage);
It looks like isPopupVisible should maybe be a state variable but I'm not sure, so left it as it is. You are also calling hidePopup() and showPopup() without pageName name in your example so they will cause an error until changed.
I'm not sure how hidePopup() and showPopup() are defined in your code, but you could potentially have this for DispatchProps instead:
interface DispatchProps {
hidePopup: typeof hidePopup;
showPopup: typeof showPopup;
}
your call to React.Component with TypeScript needs to specify Props as a type argument like so:
class AccountsPage extends React.Component<Props, {}> {
}

TypeScript errors when using Redux-Form with React-Redux connect

I was falling in love with TypeScript until I found some very discouraging incompatibilities between Redux-Form with React-Redux.
My goal is wrap a reduxForm decorated component with the react-redux connect decorator—this pattern has always worked for me in babel configurations and seems to follow the HOC methodology. Here's an example:
import * as React from 'react';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { reduxForm, Field, InjectedFormProps } from 'redux-form';
interface SampleFormData {
username: string;
}
interface SampleFormProps {
saveData: (data: SampleFormData) => void;
}
type AllSampleFormProps = SampleFormProps & InjectedFormProps<SampleFormData>;
const SampleForm: React.SFC<AllSampleFormProps> = (props) => (
<form onSubmit={props.handleSubmit(props.saveData)}>
<Field name="username" component="input" />
</form>
);
const DecoratedSampleForm = reduxForm<SampleFormData>({ form: "sampleForm" })(SampleForm);
export default connect(
() => ({}),
(dispatch) => ({
saveData: (data: SampleFormData) => dispatch({ type: "SAVE_DATA", data })
})
)(DecoratedSampleForm);
Here's the errors TypeScript is throwing:
> Argument of type 'DecoratedComponentClass<SampleFormData,
> Partial<ConfigProps<SampleFormData, {}>>>' is not assignable to
> parameter of type 'ComponentType<{ saveData: (data: SampleFormData) =>
> { type: string; data: SampleFormData; }; }>'.
>
> Type 'DecoratedComponentClass<SampleFormData,
> Partial<ConfigProps<SampleFormData, {}>>>' is not assignable to type
> 'StatelessComponent<{ saveData: (data: SampleFormData) => { type:
> string; data: SampleFormData; };...'.
>
> Type 'DecoratedComponentClass<SampleFormData,
> Partial<ConfigProps<SampleFormData, {}>>>' provides no match for the
> signature '(props: { saveData: (data: SampleFormData) => { type:
> string; data: SampleFormData; }; } & { children?: ReactNode; },
> context?: any): ReactElement<any>'.
Has anyone found a solution to make react-redux accept the DecoratedComponentClass type? I found a suggestion to use a "middle" component but I haven't managed to get this to work with thunk actions. Plus I've found that this creates more problems than it solves in terms of typing the form's props.
To anyone who comes across this, I found that I was able to dismiss the error by providing the connect statement with empty TStateProps and TDispatchProps objects.
interface SampleFormData {
username: string;
}
interface SampleFormProps {
saveData: (data: SampleFormData) => void;
}
type AllSampleFormProps = SampleFormProps & InjectedFormProps<SampleFormData>;
const SampleForm: React.SFC<AllSampleFormProps> = (props) => (
<form onSubmit={props.handleSubmit(props.saveData)}>
<Field name="username" component="input" />
</form>
);
const DecoratedSampleForm = reduxForm<SampleFormData>({ form: "sampleForm" })(SampleForm);
export default connect<{},{}>(
() => ({}),
(dispatch) => ({
saveData: (data: SampleFormData) => dispatch({ type: "SAVE_DATA", data })
})
)(DecoratedSampleForm);
The one downside to this is that it forces us to blindly supply connect props but I felt that this was a more elegant solution than writing an override #types declaration.
To address this shortcoming, I was able to validate the types by providing connect with the correct interfaces versus empty objects; however, this method can only be done temporarily to check the bindings as it doesn't resolve the DecoratedComponentClass error.
export default connect<{}, SampleFormProps, InjectedFormProps<SampleFormData>>(
() => ({}),
(dispatch) => ({
saveData: (data: SampleFormData) => dispatch({ type: "SAVE_DATA", data })
})
)(DecoratedSampleForm);
A higher Component Interface declaration does the trick that wraps up connects Type with component state and props Type using a decorator.
connect.ts
import * as React from "react";
import {
connect as originalConnect,
MapStateToPropsParam,
MergeProps,
Options
} from "react-redux";
import { IState } from "./index";
export interface IDisPatchProps {
[key: string]: () => void;
}
export type InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> = <
TComponent extends React.ComponentType<TInjectedProps & TNeedsProps>
>(
component: TComponent
) => TComponent;
export interface IConnectProps {
<TStateProps = {}, TDispatchProps = {}, TOwnProps = {}>(
mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, IState>,
mapDispatchToProps?: IDisPatchProps
): InferableComponentEnhancerWithProps<
TStateProps & TDispatchProps,
TOwnProps
>;
<TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, TMergedProps = {}>(
mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, IState>,
mapDispatchToProps?: IDisPatchProps,
mergeProps?: MergeProps<
TStateProps,
TDispatchProps,
TOwnProps,
TMergedProps
>,
options?: Options<TStateProps, TOwnProps, TMergedProps>
): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps>;
}
declare module "react-redux" {
// tslint:disable-next-line
interface Connect extends IConnectProps {}
}
export const connect = originalConnect as IConnectProps;
***ClassFile***
#connect(
(state: IState): IStateProps => ({
count: state.counter.count,
isLoading: state.counter.isLoading
}),
{
decrement,
increment
}
)
export default class MyApp
Link:
https://github.com/TomasHubelbauer/react-redux-typescript-connect-decorator-demo/blob/master/my-app/src/connect.ts
credit goes to: TomasHubelbauer https://github.com/TomasHubelbauer

Type Error when using Typescript with React-Redux

I am trying to use react-redux with typescript and I'm getting a type error when I try to inject props using connect() and mapStateToProps.
My component looks like this:
function mapStateToProps(state) {
return {
counter: state.counter
};
}
function mapDispatchToProps(dispatch) {
return {
incr: () => {
dispatch({type: 'INCR', by: 2});
},
decr: () => {
dispatch({type: 'INCR', by: -1});
}
};
}
export default class Counter extends React.Component<HelloProps, any> {
render() {
return (
<div>
<p>
<label>Counter: </label>
<b>#{this.props.counter}</b>
</p>
<button onClick={e => this.props.incr() }>INCREMENT</button>
<span style={{ padding: "0 5px" }}/>
<button onClick={e => this.props.decr() }>DECREMENT</button>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
The store looks like this
let store = createStore(
(state:HelloState, action:HelloAction) => {
switch (action.type) {
case 'INCR':
return {counter: state.counter + action.by};
default:
return state;
}
},
Finally, I have defined my types to be:
interface HelloProps {
counter?: number;
incr?: () => any;
decr?: () => any;
}
interface HelloState {
counter:number;
}
interface HelloAction {
type:string,
by:number;
}
When I try and compile the code I get the following error:
(39,61): error TS2345: Argument of type 'typeof Counter' is not assignable to parameter of type 'ComponentClass<{ counter: any; } & { incr: () => void; decr: () => void; }> | StatelessComponent<...'.
Type 'typeof Counter' is not assignable to type 'StatelessComponent<{ counter: any; } & { incr: () => void; decr: () => void; }>'.
Type 'typeof Counter' provides no match for the signature '(props?: { counter: any; } & { incr: () => void; decr: () => void; }, context?: any): ReactElement<any>'
Interestingly the code still works even though it throws the type error. Also, changing the component's prop interface to any also solves the issue. It seems like the type system doesn't understand that the two objects are merged by the two mapped functions.
To preserve type safety you can divide your props in parts, one responsible for normal props and one for dispatchers:
import * as React from 'react';
import {connect} from 'react-redux';
interface StateProps {
textPros: string,
optionalText?: string,
}
interface DispatchProps {
onClick1: Function,
}
class MyComp extends React.Component<StateProps & DispatchProps , any> {
render() {
return (<div onClick={this.props.onClick1}>{this.props.textPros}</div>);
}
}
const mapStateToProps = (state: any, ownProp? :any):StateProps => ({
textPros: "example text",
});
const mapDispatchToProps = (dispatch: any):DispatchProps => ({
onClick1: () => {
dispatch({ type: 'CLICK_ACTION'});
}
});
export default connect(mapStateToProps, mapDispatchToProps)(MyComp);
For those who search for quick workaround: just add 'as any' to base component.
export default connect(mapStateToProps, mapDispatchToProps)(MyComp as any);
I found the answer in the second to last post on this Github issue. Without the type parameter on both the mapStateToProps and/or mapDispatchToProps or on connect it will produce an intersection of the return types of the two map functions.

Resources