在 React 函数式组件中使用 TypeScript(持续更新)

407 阅读3分钟

本文参考自 react-typescript-cheatsheet ,简单介绍函数式组件的 TypeScript 写法。如果想了解类组件的写法以及更多的细节,可以阅读原文档

如果有什么补充,欢迎大家在评论区交流。

一、Props

和 TypeScript 中函数的类型声明类似:

interface Props {
  /** 标题内容 */
  text?: string
  /** 组件插槽 */
  children: React.ReactNode
}

export const Heading = (props: Props): JSX.Element => {
  const { text = 'Hello, React + TypeScript', children } = props

  return <h1>{children || text}</h1>
}

建议为每一个 prop 提供 /** comment */ 文档注释,以获得更好的代码可读性:

image-20230224223331190

二、Hooks

1. useState

useState 可以根据传入的初始值推断出数据类型:

image-20230224223907773

如果传入的是 null ,就要用到泛型和联合类型了:

interface User {
  /** 名字 */
  firstName: string
  /** 年龄 */
  age: number
}

const [state, setState] = useState<User | null>(null)
setState({ firstName: 'Jon', age: 20 })

2. useCallback

这个很简单,和 TypeScript 定义函数类型一样:

const memoCallback = useCallback((user: User) => {
  console.log(user.firstName)
}, [])

可以看一下 useCallback 的类型定义:返回值类型就是回调函数类型 T

function useCallback<T extends Function>(callback: T, deps: DependencyList): T;

3. useReducer

使用可识别的联合类型声明 action :

import { useReducer } from 'react'

const initialState = { count: 0 }

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

function reducer(state: typeof initialState, action: ACTIONTYPE) {
  // action.type 被推断为字符串字面量联合类型 "increment" | "decrement"
  switch (action.type) {
    case 'increment':
      // payload 被推断为 number 类型
      return { count: state.count + action.payload }
    case 'decrement':
      // payload 被推断为 string 类型
      return { count: state.count - Number(action.payload) }
    default:
      throw new Error('unknown action\'s type')
  }
}

export function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>
        -
      </button>
      {/* 报错:'increment' 对应的 payload 为 number 类型,不能将 string 赋给 number */}
      <button onClick={() => dispatch({ type: 'increment', payload: '5' })}>
        +
      </button>
    </div>
  )
}

4. useRef

  1. 只读,初始值是 null

    function Foo() {
      const inputRef = useRef<HTMLInputElement>(null)
      // Error: Cannot assign to 'current' because it is a read-only property.
      inputRef.current = 123
      
      return <input ref={inputRef} />
    }
    
  2. 可变

    const intervalRef = useRef<number>() // number | undefined
    useEffect(() => {
      intervalRef.current = setInterval(() => {})
      return () => clearInterval(intervalRef.current)
    }, [])
    

关于只读和可变的判断,可以看一下声明文件:

// 初始值传入 null ,表示 ref.current 只读
function useRef<T>(initialValue: T | null): RefObject<T>;
// 否则 ref.current 可变
function useRef<T = undefined>(): MutableRefObject<T | undefined>;

5. useImperativeHandle

forwardRef 接收两个泛型:

  • 暴露给外部的 handle 集合 T
  • 组件接收的 props 类型 P
/// Person.tsx
/** 暴露给外部的 handle 集合 */
export interface PersonHandle {
  say: () => void
}
interface PersonProps {
  firstName: string
}
const Person = forwardRef<PersonHandle, PersonProps>((props, ref) => {
  useImperativeHandle(ref, () => ({
    say() {
      console.log(props.firstName)
    },
  }))

  return <div>Person: {props.firstName}</div>
})

/// App.tsx
export default function App() {
  // 将 Person 组件提供的 PersonHandle 传递给 useRef 以获取类型提示
  const personRef = useRef<PersonHandle>(null)

  return <div>
    <Person ref={personRef} firstName="Jon" />
    <button onClick={() => personRef.current?.say()}>Click</button>
    {/* Error: Property 'run' does not exist on type 'PersonHandle'. */}
    <button onClick={() => personRef.current?.run()}>Click</button>
  </div>
}

6. 自定义 Hooks

需要给返回的数组(元组)做 const 断言:

export function useLoading() {
  const [isLoading, setState] = useState(false)
  const load = (p: Promise<any>) => {
    setState(true)
    return p.finally(() => setState(false))
  }
  // 无 const 断言:(boolean | ((p: Promise<any>) => Promise<any>))[] ❎
  // 调用者无法准确获取类型
  // return [isLoading, load]
  // 有 const 断言:readonly [boolean, (p: Promise<any>) => Promise<any>] ✅
 	return [isLoading, load] as const
}

如果自定义 hook 返回不止两个值,推荐使用对象代替数组(元组)。

三、事件

  • 行内事件处理函数,自带类型提示:

    image-20230225155308871
  • 单独声明的事件处理函数:

    定义格式:React.事件类型<触发元素类型>

    import React from 'react'
    
    export function App() {
      function onClick(e: React.MouseEvent<HTMLButtonElement>) {
        console.log(e.currentTarget)
      }
      return <button onClick={onClick}>Click</button>
    }
    

四、Context

// 定义 context 类型
type ThemeContextType = 'light' | 'dark'

// 使用泛型创建 context
const ThemeContext = createContext<ThemeContextType>('light')

// 子组件
function MyComponent() {
  // theme 被推断为 ThemeContextType
  const theme = useContext(ThemeContext)
  return <div>Theme is {theme}.</div>
}

export function App() {
  // 使用泛型调用 useState
  const [theme, setTheme] = useState<ThemeContextType>('light')

  function onClick() {
    setTheme(theme === 'light' ? 'dark' : 'light')
  }
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={onClick}>Toggle</button>
      <MyComponent />
    </ThemeContext.Provider>
  )
}