Inferring mapped props when using TypeScript and R

2020-04-17 05:09发布

问题:

I found a way to get type safety when using mapStateToProps from react-redux: as documented you can define an interface and parameterize React.Component<T> with your interface.

However, when I am defining mapStateToProps, I'm already defining a function where the types of the properties of the resulting object can be inferred. Eg,

function mapStateToProps(state: MyState) {
    return {
        counter: state.counter
    };
}

Here, the prop counter can be inferred to be the same type as state.counter. But I still have to have boilerplate code like the following:

interface AppProps {
    counter: number;
}


class App extends React.Component<AppProps> { ... }

export default connect(mapStateToProps)(App);

So the question is, is there any way to structure the code so that I can avoid writing the type of counter twice? Or to avoid parameterizing the type of React.Component -- even if I could have the props of the component inferred from an explicitly-hinted result type of the mapStateToProps function, that would be preferable. I am wondering if the duplication above is indeed the normal way to write typed components using React-Redux.

回答1:

Yes. There's a neat technique for inferring the type of the combined props that connect will pass to your component based on mapState and mapDispatch.

There is a new ConnectedProps<T> type that is available in @types/react-redux@7.1.2. You can use it like this:

function mapStateToProps(state: MyState) {
    return {
        counter: state.counter
    };
}

const mapDispatch = {increment};

// Do the first half of the `connect()` call separately, 
// before declaring the component
const connector = connect(mapState, mapDispatch);

// Extract "the type of the props passed down by connect"
type PropsFromRedux = ConnectedProps<typeof connector>
// should be: {counter: number, increment: () => {type: "INCREMENT"}}, etc

// define combined props
type MyComponentProps = PropsFromRedux & PropsFromParent;

// Declare the component with the right props type
class MyComponent extends React.Component<MyComponentProps> {}

// Finish the connect call
export default connector(MyComponent)

Note that this correctly infers the type of thunk action creators included in mapDispatch if it's an object, whereas typeof mapDispatch does not.

We will add this to the official React-Redux docs as a recommended approach soon.

More details:

  • Gist: ConnectedProps - the missing TS helper for Redux
  • Practical TypeScript with React+Redux
  • DefinitelyTyped #31227: Connected component inference
  • DefinitelyTyped PR #37300: Add ConnectedProps type


回答2:

I don't think so. You could make your setup more concise by using Redux hooks: https://react-redux.js.org/next/api/hooks

    // Your function component . You don't need to connect it
    const App: React.FC = () => {
      const counter = useSelector<number>((state: MyState) => state.counter);
      const dispatch = useDispatch(); // for dispatching actions
    };

Edit: You can if you just use the same MyState type. But I don't think you would want that.



回答3:

I would type mapped dispatch props, and component props separately and then combine the inferred type of the mapped state to props function. See below for a quick example. There might be a more elegant solution but hopefully, it will hopefully get you on the right track.

import * as React from "react";
import { Action } from "redux";
import { connect } from "react-redux";

// Lives in some lib file
type AppState = {
  counter: number;
};

type MappedState = {
  computedValue: number;
};
type MappedDispatch = {
  doSomethingCool: () => Action;
};
type ComponentProps = {
  someProp: string;
};

const mapStateToProps = (state: AppState) => ({
  computedValue: state.counter
});

const mapDispatchToProps: MappedDispatch = {
  doSomethingCool: () => {
    return {
      type: "DO_SOMETHING_COOL"
    };
  }
};

type Props = ReturnType<typeof mapStateToProps> &
  MappedDispatch &
  ComponentProps;

class DumbComponent extends React.Component<Props> {
  render() {
    return (
      <div>
        <h1>{this.props.someProp}</h1>
        <div>{this.props.computedValue}</div>
        <button onClick={() => this.props.doSomethingCool()}>Click me</button>
      </div>
    );
  }
}

const SmartComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(DumbComponent);

export default SmartComponent;