React + TypeScript 使用指南

1,293 阅读6分钟

最近总结了一些 React + TS 的使用经验,欢迎大家指正~
TS官方文档

目录

  • 简单的例子
  • 组件属性常用类型
  • 组件默认属性
  • React 实例类型
  • React Hooks 中使用 TS
  • 常用的 TS 操作符
  • 其他补充

简单的例子

React.FC 泛型中接收组件 props 的类型

// 也可以用 type TestCompProps = {}
interface TestCompProps {
  username: string
  status?: 'wait' | 'pass' | 'reject' // 可选属性
  onChange: (index: number) => void
}

const TestComp: React.FC<TestCompProps> = (props) => {
  const { username, status, onChange } = props
  
  return (
    <div>
      <p>用户名:{username}</p>
      {status && (
        <p>状态:{status}</p>
      )}
      <button onClick={() => onChange(66)}>点我</button>
    </div>
  )
}

组件属性常用类型

基本

interface PropsType {
  title: string
  count: number
  disabled: boolean
  other?: string // 可选属性
}

数组

interface PropsType {
  infoList: string[] // 等同 Array<string>
  // 数组项是对象
  objArr: {
    adId: string
    mediaUrl: number
  }[]
  excelRow: [number, string, string] // 一般用于确定长度的数组
  status: 'success' | 'fail' | 'wait' // |表示或
  twoDimArr: any[][] // 任意类型的二维数组
}

对象

interface PropsType {
  obj1: object // 任何对象 但不可访问属性(不建议用)
  obj2: {} // 和object一样
  // 普通
  obj3: {
    id: string | number
    title: string
  }
  // 不确定键名
  looseObj1: {
    [key: string]: { id: number; title: string }
  }
  // 同上,Record会对键值对进行映射
  looseObj2: Record<string, { id: number; title: string }>
}

函数

interface PropsType {
  // 函数类型 但不需要调用的(不推荐使用)
  onSomething: Function
  // 最简单的,void表示没有返回值
  onDoSometing: () => void
  // 带参数的函数
  onChange: (index: number) => void
  // dom事件函数(这里是鼠标事件 HTMLButtonElement表示<button />元素)
  onClick(event: React.MouseEvent<HTMLButtonElement>): void
}

DOM 相关的TS类型 基本都在 lib.dom.ts

组件默认属性

可以合并组件默认属性的类型

interface CompType {
  id: number
  title: string
}

const defaultProps = { age: 18 }

const Comp: React.FC<CompType & typeof defaultProps> = ({ id, title, age }) => {
  return <div>ID:{id},标题:{title},年龄:{age}</div>
}
Comp.defaultProps = defaultProps

const TestPage: React.FC = () => {
  return (
    <div>
      <Comp title="20211202世界完全对称日" id={886} />
    </div>
  )
}

对于上面的合并默认属性类型的情况 又想复用这个组件的类型,直接使用 props: React.ComponentProps<typeof Comp> 会报错!需要自己创建一个泛型来解决:

下面的 ComponentProps 意思是 当 T 继承自 React.ComponentType<infer P>React.Component<infer P> 时,返回 JSX.LibraryManagedAttributes<T, P> 否则为 never
TCompType & typeof defaultPropsP 是当前组件里传了什么属性

type ComponentProps<T> = T extends
  | React.ComponentType<infer P>
  | React.Component<infer P>
  ? JSX.LibraryManagedAttributes<T, P>
  : never

const GreetComp = (props: ComponentProps<typeof Comp>) => {
  const { id, title, age } = props
  return <div>ID:{id},标题:{title},年龄:{age}</div>
}

infer 一般与 extends 配合使用,这里使用 infer 对未知类型 P 进行类型推断
TS 内置类型 ReturnType 的原理实现就使用了 infer

// 用于提取函数类型的返回值类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

React 实例类型

interface AppProps2 {
  children1: JSX.Element // html节点
  children2: JSX.Element | JSX.Element[]
  children3: React.ReactChildren // 只是别名 没有真实作用(不建议使用)
  children4: React.ReactChild[] // 比ReactChildren好一些(不建议使用)
  // 接收所有的React类型 包括null(比如JSX.Element、React.ReactChild都属于它)
  children: React.ReactNode 
  functionChildren: (name: string) => React.ReactNode
  style?: React.CSSProperties // css属性
  // 表单事件函数
  onChange?: React.FormEventHandler<HTMLInputElement>
}

JSX.Element 必须是纯粹的html节点 不接收字符串,比如 div,一般使用 React.ReactNode 是最佳选择,有个技巧 比如 HTMLElement 当不知道具体使用哪种类型时,在 vscode 中按住 ctrl 点击某个类型可以查看 TS 的类型定义源文件

React Hooks 中使用 TS

useState

可以自动推断类型,也可以接收一个泛型

type IUserInfo = {
  id: number
  username: string
}

const App = () => {
  const [ logged, setLogged] = useState(false) // 类型自动推断
  const [ userInfo, setUserInfo ] = useState<IUserInfo | null>(null)
  // ...
}

useEffect

useEffect(() => {
  let timer = setTimeout(() => {
    console.log('do somthing!')
  }, timerMs)
  // 组件卸载时
  return () => {
    clearTimeout(timer)
  }
}, [props.times])

useRef

一般用于保存任何非作用于视图更新的可变值,和 DOM 引用,视图更新用 useState

const TextInputWithFocusButton: React.FC = () => {
  const inputEl = useRef<HTMLInputElement>(null)
  const onButtonClick = () => {
    // .current 指向下面已挂载到 DOM 上的 <input/> 元素
    inputEl.current?.focus()
  }
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus!</button>
    </>
  )
}

有个小技巧,如果觉得每次写 inputEl.current?. 比较麻烦,可以使用 ! 告诉 TS inputEl 一定是有值的

// ...
const inputEl = useRef<HTMLInputElement>(null!)
const onButtonClick = () => {
  inputEl.current.focus() // OK
}
// ...

另外对于 setTimeoutsetInterval 的类型可以使用 NodeJS.Timeout

// ...
const timer = useRef<NodeJS.Timeout | null>(null)
const onButtonClick = () => {
  timer.current = setTimeout(() => {}, 1000)
}
// ...

useReducer

state 逻辑比较复杂时,使用 useReducer 更合适

import { useReducer } from 'react'

type ActionType =
  | { type: 'increment'; payload: number }
  | { type: 'decrement'; payload: string }

// 状态初始值
const initialState = { count: 1 }

// 如果明确知道值的类型,直接用typeof进行推断 比较方便
function reducer(state: typeof initialState, action: ActionType) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + action.payload }
    case 'decrement':
      return { count: state.count - action.payload }
    default:
      throw new Error()
  }
}

const Counter: React.FC = () => {
  const [ state, dispatch ] = useReducer(reducer, initialState)
  return (
    <div>
      <button onClick={() => { dispatch({ type: 'decrement', payload: 1 }) }}>减</button>
      <div>{state.count}</div>
      <button onClick={() => { dispatch({ type: 'increment', payload: 1 }) }}>加</button>
    </div>
  )
}

useImperativeHandle 句柄

待更新……

自定义hooks

点击目标元素之外区域的自定义hook,参数 ref 类型为 React.RefObject

function useOutsideClick(ref: React.RefObject<HTMLElement>, fn: () => void) {
  const handleClickOutside = (e: MouseEvent) => {
    // 如果被点击的元素不包含目标元素,就调用 fn 回调函数
    if (ref.current && !ref.current.contains(e.target as HTMLElement)) fn()
  }

  useEffect(() => {
    document.addEventListener('click', handleClickOutside)
    // 组件卸载时移除事件监听
    return () => {
      document.removeEventListener('click', handleClickOutside)
    }
  }, [ref])
}

使用上面hook

const Comp: React.FC = () => {
  const btnEl = useRef<HTMLButtonElement | null>(null!)
  useOutsideClick(compRef, () => {
    // do somthing...
  })
  return <button ref={btnEl}>目标元素</button>
}

常用的 TS 操作符

?: 可选参数

type IUser = {
  id: number
  avatar?: string // 可选的
}

?. 可选链操作符

在类型中 ?: 表示可选参数,?. 是可选链操作符(TypeScript 3.7+)当访问对象属性时 如果属性不存在就返回 undefind 存在就直接返回属性值(如果属性有值为 null 就返回 nullundefined 就返回 undefined,不会混淆影响)

// 可选链操作符
const obj = { age: 18 }
const val = obj?.title // undefined
let result = obj.fn?.() // obj上有fn函数就调用 没有则不调用

上面编译成 ES5

var val = a === null || a === void 0 ? void 0 : a.b;

?? 空值合并操作符

用于变量或属性赋值:?? 左边数据为 nullundefined 就赋值右边的,设置默认值。
用于函数:?? 左边的函数返回不为 false 就执行右边函数。

// 1.设置默认值
const theFoo = obj.foo ?? '123'

// 2.函数操作
function leftFuncOk1() {
  return null // undefined也一样
}
function leftFuncOk2() {
  return true
}
function leftFuncNo() {
  return false
}
function print() {
  console.log('有结果~')
}
// 这两都会执行print!
leftFuncOk1() ?? print()
leftFuncOk2() ?? print()
// 则不会执行print
leftFuncNo() ?? print()

另一种是 ??=,同理当变量为 nullundefined 时,就就赋值右边的

// 这里foo被设为66
let foo = null
foo ??= 66

! 非空断言操作符

! 本身可置反布尔值,但如果放在一个变量后面叫 非空断言操作符,即告诉 TS 该属性肯定是有值的,不会是 nullundefined(避免由此引起的一些类型错误)

// 非空断言操作符
// #1
let x!: number
// #2
const boxWidth = document.querySelector('#box')!.offsetWidth
// #3
const el = useRef<HTMLDivElement | null>(null!)

| & 运算符

| 是联合类型,用在基本类型时 可以理解为“或”

const todo = (id: string | number) => {
  console.log(id)
}
// OK
todo(123)
todo('123')

type StatusType = 'success' | 'fail'
let resStatus: StatusType = 'success'
resStatus = 'wait' // 报错

如果是 type(类型别名)或 interface(接口)的联合类型,新声明的对象可以包含几个类型的所有属性,但只能访问它们共有的属性,比如下面只能访问 obj.name

interface Foo {
  name: string
  age: number
}

interface Bar {
  name: string
  onChange: () => void
}

// OK
let obj: Foo | Bar = {
  name: 'Li',
  age: 18,
  onChange: () => { console.log(123) }
}

console.log(obj.name) // OK
console.log(obj.age) // 报错
obj.onChange() // 报错

& 是交叉类型,修改下上面的 obj 为交叉类型,obj 的属性必须同时满足 FooBar,一般用于合并两个类型

// 报错!
let obj: Foo | Bar = {
  name: 'Li',
  age: 18
}

// OK
let obj: Foo | Bar = {
  name: 'HaHa',
  age: 20,
  onChange: () => { console.log(123) }
}

其他补充

不建议随便使用 any,有办法时尽量不用,如果不知道是什么类型 可以用泛型解决

======== 将不定期更新 ========