typescript+react+redux 踩坑记录

1,059 阅读4分钟

前言 之前学习了用 react + redux 实现一个简易的 todolist Demo,感觉挺简单的,最近又自己看了些 typescript 的教程,打算用 typescript 重新实现一次,发现原来没那么容易.遇到好多的坑.记录一下

原生 JavaScript 组件写法

import React, { Component } from 'react';
class ComponentName extends Component {
render() { 
    return (  
    <div>hello World</div>    
    ) }
}
export default ComponentName;

typescript 组件写法

interface IPorps{  name:string}
interface IState{  count:number}
class ComponentName extends Component<IProps,IState> {
    render() {   
    return (
    <div>hello World</div>
    ) }
}
export default ComponentName;

可以看到,与原生 js 写法不同,ts 组件Component 后面多了<IProps,IState> ,这里方括号表示的是泛型约束,里面的变量 IPorps, IState 是代表要传入组件的 props 和 state 必须要有与 IProps,IState 所约束类型的值,以及相同返回类型的同名方法, 简单的来说就是

    interface mustHasLength {
        length:number
    }
    
    function printArgLenth <T extend mustHasLength> (arg:T) : void {
        console.log(arg.length)
    }
    
   let arr = [1,2,3],
     objWidthOutLength = {
       id:1,
       name:'obj1'
   },
    objWidthLength = {
        id:2,
        name:'obj2',
        length:3
    }
    console.log(printArgLength(arr)) // 输出 3
    console.log(printArgLength(objWidthLength))  // 输出 3
    
    console.log(printArgLength(objWidthOutLength)) // 报错
    类型“{ id: number; name: string; }”的参数不能赋给类型“mustHasLength”的参数。
  Property 'length' is missing in type '{ id: number; name: string; }' but required in type 'mustHasLength'.ts(2345)
   

从上面的代码可以看出泛型约束的作用,当参数arg含有 length这个属性时,编译会正常通过,输出 arg.length, 当参数 arg 不含有 length 这个属性时,编译会不通过,并且提示代码出错的地方,让我们更好思考代码组织和定义

在 ts 里面,类型检查是很严格的,如果传递给组件的类型和在方括号泛型声明的类型不匹配,编译都不会通过.在写组件之前,要想好这个组件需要用到什么属性和状态,然后再定义接口,把用到的 props,state 的类型都写上.

import React, { MouseEvent, ChangeEvent, PureComponent } from 'react'
import { TodosState, TodoTypes } from './types'
import { connect } from 'react-redux'
import { addTodo, change_input_value } from './actions'

interface PropsFromDispatch {
  addTodo: typeof addTodo
  change_input_value: typeof change_input_value
}
interface PropsFromState {
  inputValue: string
  data: TodoTypes[]
}

type AllProps = PropsFromDispatch & PropsFromState

class AddToto extends PureComponent<AllProps> {
  render() {
    return (
      <div>
        <input type='text' onChange={this.handleChange} />
        <button type='button' onClick={this.handleSubmit}>
          submit todo
        </button>
      </div>
    )
  }
  handleSubmit = (e: MouseEvent): void => {
    if (this.props.inputValue !== '') {
      this.props.addTodo(this.props.inputValue)
    }
  }

  handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
    let value = e.currentTarget.value
    this.props.change_input_value(value)
  }
}

const mapDispatchToProps = {
  addTodo,
  change_input_value
}
const mapStateToProps = (state: TodosState) => {
  return {
    inputValue: state.inputValue,
    data: state.data
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(AddToto)

在我这个 AddTodo组件中,我使用了 react-redux 的 connect 方法把从 redux 中获取的属性和方法都挂载到组件的 props 上,所以定义了两个接口 PropsFromDispatch PropsFromState ,然后把它们结合的类型通过 AllProps 传递给组件,这样组件才能知道将来要使用的方法和属性是什么类型的.

types.ts

export interface TodoTypes {
  id: number
  name: string
}
export interface TodosState {
  readonly inputValue: string
  readonly data: TodoTypes[]
}

export interface TodoDispatch {
  addTodo?: () => void
  change_input_value?: () => void
  deleteToDo?: () => void
}

export enum TodoActionType {
  ADD_TODO = 'ADD_TODO',
  DELETE_TODO = 'DELETE_TODO',
  CHANGE_INPUT_VALUE = 'CHANGE_INPUT_VALUE'
}

这里分别定义了三个接口,TodoTypes,TodosState,TodoDispatch 以此来通知 reducer 即将用到的 state 的类型,以及使用枚举类型(enum)定义 redux 中的 行为标识(actionTypes).

reducer.ts

import { Reducer } from 'redux'
import { TodoActionType, TodosState } from './types'
 const initialState: TodosState = {
 inputValue: '',  
 data: [ 
    {      id: 0,      name: '睡觉觉'    },  
    {      id: 1,      name: '睡觉觉'    },   
    {      id: 2,      name: '睡觉觉'    }  ]
     
 }
 const reducer: Reducer<TodosState> = (state = initialState, action) => { 
 let newState = JSON.parse(JSON.stringify(state)) 
 switch (action.type) {   
    case TodoActionType.ADD_TODO:         
        return addtodo()  
    case TodoActionType.CHANGE_INPUT_VALUE:   
        return changeInput()    
    case TodoActionType.DELETE_TODO:
        return deleteTodo()   
    default:      
    return newState  
    }
 }
 export { reducer as todosReducer }

在 reducer.ts 中,使用到TodosState接口定义初始 state的类型,然后根据不同的行为标识(TodoActionType),返回不同函数运行,这三个函数都用于返回新的状态(state).

actions.ts

import { action } from 'typesafe-actions'
import { TodoActionType } from './types'

export const addTodo = (payload: string) => action(TodoActionType.ADD_TODO, payload)
export const deleteTodo = (payload: number) => action(TodoActionType.DELETE_TODO, payload)
export const change_input_value = (payload: string) => action(TodoActionType.CHANGE_INPUT_VALUE, payload)

在 actions.ts 中,用到了第三方库 typesafe-actions用于创建类型安全的 action

从上面三个文件可以看出,使用 typescript 对比原生写法,多了很多类型的定义或着说明,类型这给概念是很重要的,没有明确告诉编译器类型,编译都不会通过.

刚开始写 typescript 和 react 结合的时候很不习惯,基本上写几句就要 debug,各种编译不通过,在参考了网上其他人的项目后,再根据编译的提示,慢慢的了解了 typescript 的类型概念,基本上算是入门了 typescript.