使用 Typescript 踩 react-redux 的坑

markzzw發表於2019-03-04

背景

最近在看antd原始碼,也在自學一些typescript的語法以及使用,然後準備帶在學校的師弟做一個音樂播放器作為typescript的練習踩踩坑;

然後竟然沒想到在專案剛開始就深陷大坑--搭建react-redux專案流程,一下內容就是記錄我是怎樣從這個深坑中爬出來的過程。

開始?根本開始不起來

從專案建立之後就掉坑裡了。。。

建立專案

建立專案的流程很簡單,可有參考我之前寫的這篇文章

從mapStateToProps&mapDispatchToProps開始爬坑

剛開始還不是很熟悉typescript,所以我最開始的reducer, action, constant,檔案都是以.js結尾,只有元件的程式碼是以.tsx結尾的,然後就開始按照流程,將store中的state進行mapStateToProps以及actions進行mapDispatchToProps,誰想到這竟然是噩夢的開始,最開始我是這樣寫的程式碼:

  import { bindActionCreators } from 'redux';
  function mapStateToProps(state: {}) {
    return {
      player: state.PlayerStore,
    };
  }

  function mapDispatchToProps(dispatch: Function) {
    return {
      playerActions: bindActionCreators(actions, dispatch)
    };
  }複製程式碼

然後就開始報錯了。。。

1
1

幾經折騰之後我又將程式碼改成了這樣,才得以過關

  import { bindActionCreators, Dispatch } from 'redux';
  function mapStateToProps(state: { PlayerStore: object }) {
    return {
      player: state.PlayerStore,
    };
  }

  function mapDispatchToProps(dispatch: Dispatch<{}>) {
    return {
      playerActions: bindActionCreators<{}>(actions, dispatch)
    };
  }複製程式碼

爬坑react元件斷言型別

  interface PlayerPropsClass {
    player: PlayerStateTypes;
    playerActions: actions.PlayerActionsTypes;
  }
  class Player extends React.Component<PlayerPropsClass, {}> {
    constructor(props: object) {
      super(props as PlayerPropsClass);
      this.state = {};
    }

    addOne = (num: number) => this.props.playerActions.count(num);

    subtractOne = (num: number) => this.props.playerActions.subtract(num);

    render() {
      const { countNumber } = this.props.player.toJS();
      return (
        <div className="player">
          <span>{countNumber}</span>
          <button onClick={() => this.addOne(countNumber as number)}>點選+1</button>
          <button onClick={() => this.subtractOne(countNumber as number)}>點選-1</button>
        </div>
      );
    }
  }

  export default connect(mapStateToProps, mapDispatchToProps)(Player as any);複製程式碼

這裡的(Player as any)報錯提示

  [tslint] Type declaration of 'any' loses type-safety. Consider replacing it with a more precise type, the empty type ('{}'), or suppress this occurrence. (no-any)複製程式碼

然後我改成

  export default connect(mapStateToProps, mapDispatchToProps)(Player as {});複製程式碼

仍然報錯

  [ts]
  型別“{}”的引數不能賦給型別“ComponentType<{ player: object; } & { playerActions: {}; }>”的引數。
  不能將型別“{}”分配給型別“StatelessComponent<{ player: object; } & { playerActions: {}; }>”。
  型別“{}”提供的內容與簽名“(props: { player: object; } & { playerActions: {}; } & { children?: ReactNode; }, context?: any): ReactElement<any> | null”不匹配。複製程式碼

哇心態爆炸,都不知道怎麼寫這個斷言。。。然後我想起了antd的一個例子,寫的是React.ReactElement,然後偶然間在網上找到了說的是在node_modules中有一個@type資料夾就是typescript對於node包中的相應的型別匹配,於是我就按照這個路徑node_modules/@types/react/index.d.ts,終於找到了React.ComponentType<T>,這個檔案裡面還有很多的型別,想要了解的可以自己去看看

  export default connect(mapStateToProps, mapDispatchToProps)(Player as React.ComponentType<PlayerPropsClass>);複製程式碼

爬坑action和reducer

有了以上的經驗,再加上在這裡看了一些程式碼,我決定把actions和reducer也改為ts結尾的檔案;

  • src/reducers/index.ts

      import { combineReducers, createStore } from 'redux';
      import PlayerStore from '../containers/Player/reducer';
    
      // 這個是用來使用到mapStateToProps函式的地方給state進行型別檢測的
      export interface AppStoreType {
        PlayerStore: object;
      }
    
      const rootReducer = combineReducers({
        PlayerStore,
      });
    
      export default () => {
        return createStore(rootReducer);
      };複製程式碼
  • containers/Demo/reducer.ts

      import * as PlayerTypes from './constant';
      import { fromJS } from 'immutable';
      // 由於使用到immutable,state上面就會使用到immutable的函式,所以需要將其也做成型別檢測
      import { ImmutableFuncType } from '../../constant';
    
      interface ActionType {
        type: string;
        countNumber?: number;
      }
    
      interface StoreType {
        countNumber: number;
      }
      // 把當前容器的state組,然後丟擲提供使用
      export type PlayerStateTypes = StoreType & ImmutableFuncType;
    
      const PlayerStore: PlayerStateTypes = fromJS({
        countNumber: 0,
      });
    
      export default (state = PlayerStore, action: ActionType) => {
        switch (action.type) {
          case PlayerTypes.COUNT:
            return state.update('countNumber', () => fromJS(action.countNumber));
          default:
            return state;
        }
      };複製程式碼
  • containers/Demo/action.ts

      import * as PlayerTypes from './constant';
    
      export const count = (num: number) => ({
        type: PlayerTypes.COUNT,
        countNumber: num + 1,
      });
    
      export const subtract = (num: number) => ({
        type: PlayerTypes.COUNT,
        countNumber: num - 1,
      });
      // 丟擲actions函式的型別以供mapDispatchToProps使用
      export interface PlayerActionsTypes {
        count: Function;
        subtract: Function;
      }複製程式碼
  • containers/Demo/index.tsx

      import * as React from 'react';
      import './style.css';
      import { connect } from 'react-redux';
      import { bindActionCreators, Dispatch } from 'redux';
      import * as actions from './action';
      import { PlayerStateTypes } from './reducer';
      import { AppStoreType } from '../../reducers';
    
      interface PlayerPropsClass {
        player: PlayerStateTypes;
        playerActions: actions.PlayerActionsTypes;
      }
    
      function mapStateToProps(state: AppStoreType) {
        return {
          player: state.PlayerStore,
        };
      }
    
      function mapDispatchToProps(dispatch: Dispatch<{}>) {
        return {
          playerActions: bindActionCreators<{}>(actions, dispatch)
        };
      }
    
      class Player extends React.Component<PlayerPropsClass, {}> {
        constructor(props: object) {
          super(props as PlayerPropsClass);
          this.state = {};
        }
    
        addOne = (num: number) => this.props.playerActions.count(num);
    
        subtractOne = (num: number) => this.props.playerActions.subtract(num);
    
        render() {
          const { countNumber } = this.props.player.toJS();
          return (
            <div className="player">
              <span>{countNumber}</span>
              <button onClick={() => this.addOne(countNumber as number)}>點選+1</button>
              <button onClick={() => this.subtractOne(countNumber as number)}>點選-1</button>
            </div>
          );
        }
      }
    
      export default connect(mapStateToProps, mapDispatchToProps)(Player as React.ComponentType<PlayerPropsClass>);複製程式碼

結尾

通過這個小練習還真的是感受到了typescript的好處,那就是在編譯時就報錯,而且能夠知道報錯原因,不需要在編譯完成以後再去進行錯誤點的查詢,確實節省了很多時間,尤其是對於JavaScript程式碼的undefined和null的處理很友好。。總之,感覺寫起來很流暢啊。啊哈哈哈哈~~~

相關文章