两天搞定用 TypeScript 进行开发(二)

148 阅读7分钟

1. useState

useState 接收一个泛型参数,用于指定初始值的类型

const [name, setName] = useState<string>('张三')
const [age, setAge] = useState<number>(28)
const [isProgrammer, setIsProgrammer] = useState<boolean>(true)

// 如果你在 setName 函数中的参数不符合声明的变量类型,程序会报错
<button onClick={() => setName(100)}>按钮</button>

注意:useState 的类型推断,在使用 useState 的时候,只要提供了初始值,TypeScript 会自动根据初始值进行类型推断,因此 useState 的泛型参数可以省略

2. useEffect

useEffect 是用于我们管理副作用(例如 API 调用)并在组件中使用 React 生命周期的

重点:useEffect 函数不涉及到任何泛型参数,在 TS 中的使用和 JS 中完全一致

// 定时器开启和关闭
useEffect(() => {
    let timer = setInterval(() => {
        console.log('哈哈哈')
    })
    return () => {
        clearInterval(timer)
    }
}, [])

// 事件的绑定和解绑
useEffect(() => {
    // 给 window 绑定点击事件
    const handleClick = () => {
        console.log('哈哈哈')
    }
    window.addEventListener('click', handleClick)

    return () => {
        // 给 window 移除点击事件
        window.addEventListener('click', handleClick)
    }
}, [])

3. 请求数据

如果 useState 没有提供具体类型的初始值,是需要使用泛型参数指定类型的

当然这些的数据通常要放在 redux 中进行状态管理,如果不放,需要定义一个初始状态类型,不然无法使用点语法

// 三种解决方案
// 方案一 使用 any 解决,但不建议
<ul>
    {list.map((item: any) => {
        return <li key={item.id}>{item.name}</li>
    })}
</ul>

// 方案二 给循环时的 item 指定类型
<ul>
    {list.map((item: TItem) => {
        return <li key={item.id}>{item.name}</li>
    })}
</ul>

// 方案三 给 useState 指定泛型参数(推荐)
// 解决1:给个初始值,不推荐
    // const [list, setList] = useState([{ name: 'ifer', id: 0 }])
    // 解决2:泛型参数
    // 一般复杂的类型,需要手动进行指定初始值类型,TS 没法进行推断
interface IList {
  name:string,
  id:number
}
// const [count,setCount] = useState()// 什么都不声明 泛型参数为 undefined 
// const [count,setCount] = useState({})// 泛型参数为 {}
// const [list,setList] = useState([])// 泛型参数为 never[] 后续取用 item 就会报错 
const [list,setList] = useState<IList[]>([])// 正确 <> 泛型参数定义 list 的类型,小括号里面的为list 的初始值

// 了解
interface IRes extends Array<{ id: number; name: string }> {}

4. useRef

使用 useRef 配合 TS 操作 DOM

useRef 接收一个泛型参数,泛型参数用于指定 current 属性的值的类型

 import { useRef } from 'react'
 export default function App() {
     // 不推荐 any
     // const inputRef = useRef<any>(null)
     // 指定了 current 的类型,目的是为了让 current 有属性提示
     // 初始为 null 是一个固定写法,制定了泛型参数是为了有提示
     const inputRef = useRef<HTMLInputElement>(null)
     const aRef = useRef<HTMLAnchorElement>(null)
     const get = () => {
         // inputRef.current 可能是 null,所以用了 ?.
         console.log(inputRef.current?.value)
         console.log(aRef.current?.href)
     }
     return (
         <div>
             <input type='text' ref={inputRef} />
             <a href='https://www.baidu.com' ref={aRef}>
                 百度
             </a>
             <button onClick={get}>获取</button>
         </div>
     )
 }

使用鼠标悬停在 ref 上悬停之后可以看到 dom对象 的类型

image-20220625224445111.png

为什么参数要是 null 或者可以是 null 呢

// 通过类型定义文件得知:参数要么是 T 类型,要么是 null
function useRef<T>(initialValue: T | null): RefObject<T>

不指定具体的类型(泛型参数)的话,inputRef.current 就为 null ,无法赋值。想要赋值必须指定具体的泛型参数

理解:

// 获取 dom 对象或者组件实例
// 为什么 inputRef.current 给了具体的泛型参数的类型,为什么初始值还可以取 null 
// 原因:见下图,内部规定了可以并上一个 null 类型,小括号里面给上别的数据 比如不给就报错
const inputRef = useRef<HTMLInputElement>(null)
const inputRef = useRef<InputRef>(null)(第三方组件的

image-20220628165657393.png

当作全局变量来用(必须要在后面并上一个 null,官方说的,想要这个 ref .current 可以改变需要并上 null 类型,获取的 dom 不需要改变就不用并)

// 存一个定时器的 number

  // 给个初始值,推导出来就是 number 类型,才可以赋值定时器 ID
  const timeIdRef = useRef(-1)
  // 另一种做法
  // const timeIdRef =useRef<number | null>(null)

5. 非空断言

  • 如果我们明确的知道对象的属性一定不会为空,那么可以使用非空断言 !
  • 注意:非空断言一定要确保有该属性才能使用,不然使用非空断言会导致 Bug
// 注意测试的时候要开启 strictNullChecks 模式
function show(name: string | undefined) {
    let sName: string = name // Error
}

// 解决
function show(name: string | undefined) {
    let sName: string
    if (name) {
        sName = name
    }
}

// 优化
function show(name: string | undefined) {
    // name! 意思是从 name 可能的值中断言(假定)没有 null 和 undefined
    let sName: string = name!
}

// 应用场景
import { useRef } from 'react'
export default function App() {
    const inputRef = useRef<HTMLInputElement>(null)
    const get = () => {
        // 断言 inputRef.current 不可能为空
        /* const current = inputRef.current!
        console.log(current.value) */
        console.log(inputRef.current!.value)
    }
    return (
        <div>
            <input type='text' ref={inputRef} />
            <button onClick={get}>获取</button>
        </div>
    )
}

注意,使用非空断言时要想明白,否则程序可能报错

const str: string | null = null
console.log(str!.length)

6. React 路由

6.1. useHistory

问题:

  • from 没有提示和类型校验(跟写 js 没区别了)(即使是随便写的你自己定义的变量,也需要提前定义好类型,写什么才能有提示)
  • 不能用 any

变量后面跟着<>并且仍然表示类型的一定是泛型接口,如H.History<HistoryLocationState>

seHistory 实现跳转功能,和 JS 中使用语法一致

// /pages/Home.tsx
import { useHistory } from 'react-router-dom'

export default function Home() {
    const history = useHistory()
    const login = () => {
        history.push('/login')
    }
    return (
        <div>
            <h2>Home</h2>
            <button onClick={login}>登录</button>
        </div>
    )
}

// useHistory 在跳转时可以通过 state 进行传参,并通过泛型参数来指定 state 的类型
// 记住即可,useHistory 的泛型参数用来指定编程式导航的参数 state 的类型
const history = useHistory<{ from: string }>()
const login = () => {
    history.push({
        pathname: '/login',
        state: {
            from: 'ifer',
        },
    })
}

写 ts 就要慢慢地想类型,如果卡住了不知道写什么类型,先用 any 或者 unknown 保证代码能跑

(location.state as any).form // 这样就不会报错了,any 想写什么就写什么

想要知道原来,可以按住 ctrl + 鼠标左键进行查看源码

6.2. useLocation

useLocation 接收一个泛型参数,用于指定接收 state 的类型,与 useHistory 的泛型参数对应

import { useLocation } from 'react-router-dom'

export default function Login() {
    const location = useLocation<{ from: string } | null>()
    // 直接点击登录页,没有传参会报错,所以这里用了可选链操作符 ?.
    return <div>Login: {location.state?.from}</div>
}

image-20220625225754569.png 优化:因为 useLocation 和 useHistory 都需要指定 Location 类型,因此可以将类型存放到通用的类型声明文件中,src/types/data.d.ts

// Tip: 这里明确或了一个 null,当后面再书写 location.state.from 的时候,.from 的前面会自动加上 ? 号
export type LocationState = {
    from: string
} | null

// src/types/data.d.ts
import { useLocation } from 'react-router-dom'
import { LocationState } from '../types'

export default function Login() {
    const location = useLocation<LocationState>()
    return <div>Login: {location.state?.from}</div>
}

data.d.ts 和 store.d.ts 使我们自己定义的类型声明文件,并不是同第三库一样的同名文件,需要我们额外的进行 export 使用

location.pathname不包括参数,只有路径名称

location.search是后面的参数,?key=a/:id这种直接使用 useParams hook 进行获取)

6.3. useParams

useParams 接收一个泛型参数,用于指定 params 对象的类型

// App.tsx
import { BrowserRouter as Router, Link, Route } from 'react-router-dom'
import Article from './Article'

export default function App() {
    return (
        <div>
            <Router>
                <nav>
                    <Link to='/article/1'>文章1</Link>
                    <Link to='/article/2'>文章2</Link>
                </nav>
                <Route path='/article/:id' component={Article} />
            </Router>
        </div>
    )
}

// pages/article.tsx
import { useParams } from 'react-router'
export default function Article() {
    const params = useParams<{ id: string }>()
    return <div>Article: {params.id}</div>
}

总结:定义任何变量之前都需要写定义类型,不要可能会报错,没有提示,失去了 ts 的意义

7. redux-thunk

react-redux@8.x.x 有些小 bug 使用 @7.2.8 的更加稳定 (useEffect 里面的 dispatch 会报错)

useEffect(()=>{
 dispatch(getChannelList())
},[])

redux-thunk @3.x.x 的版本 里面的提示出不来,换成 @2.3.0 的就有了(action 里面的 dispatch 即异步 return 的回调里面,没有提示,但是会有警告)

export const getChannel = (): RootThunkAction => {
return async (dispatch) => {
 // get 的泛型参数其实限制的是 res.data 的类型
 const res = await axios.get<IResponse<{ channels: ChannelItem[] }>>('http://geek.itheima.net/v1_0/channels')
 // res.data 再往后面点的话没有提示
 dispatch({
   type: 'CHANNEL_SAVE',
   payload: res.data.data.channels,
 })
}
}

写项目也可以不用 ts 写,用 js 写也可以

如何处理定义在 action 中的异步函数的返回值的类型

ThunkAction 类型的使用,参考文档

// 泛型参数
// 1: 指定内部函数的返回值类型,一般是 void
// 2: 指定 RootState 的类型
// 3: 指定额外的参数类型,这里用不到,一般为 unknown 或 any,可以在配置 redux-thunk 的时候,通过 thunk.withExtraArgument('ifer') 指定
// 4: 指定 dispatch 的 action 的类型
import { ThunkAction } from 'redux-thunk'
export const todoDelAsync = (id: number): ThunkAction<void, RootState, unknown, TodoAction> => {
    // 后面三个参数是啥,看下文档
    return (dispatch, getState, extraData) => {
        // getState().todo // 因为,指定了 RootState 类型,这儿自动具有提示
        setTimeout(() => {
            dispatch(todoDel(id))
        }, 2000)
    }
}

不定义这个函数回调的类型,回到函数里面校验也不校验,提示也没有提示

以后的统一写法

import { ThunkAction } from 'redux-thunk'
import store from '../store'

export type TodoAction =
    | {
          type: 'TODO_ADD'
          name: string
          id: number
          done: boolean
      }
    | {
          type: 'TODO_DEL'
          id: number
      }
    | {
          type: 'TODO_CHANGE_DOEN'
          id: number
      }
export type RootAction = TodoAction // 后面可以并上别的模块的
export type RootState = ReturnType<typeof store.getState>
export type RootThunkAction = ThunkAction<void, RootState, unknown, RootAction>