精彩文章免费看

React + TS 实践

React + TS 实践

引入 React

import * as React from 'react'
import * as ReactDOM from 'react-dom'
  • 需要添加额外的配置"allowSyntheticDefaultImports": true
    import React from 'react'
    import ReactDOM from 'react-dom'
    

    函数式组件声明方式

  • 推荐 React.FunctionComponent === React.FC(我们项目中也是这么用的)
  • // Great
    type AppProps = {
      message: string
    const App: React.FC<AppProps> = ({ message, children }) => (
        {message}
        {children}
    

    使用用 React.FC 声明函数组件和普通声明以及 PropsWithChildren 的区别是:

  • React.FC 显式地定义了返回类型,其他方式是隐式推导的

  • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全

  • React.FC 为 children 提供了隐式的类型(ReactElement | null)

  • 比如以下用法 React.FC 会报类型错误:

    const App: React.FC = props => props.children
    const App: React.FC = () => [1, 2, 3]
    const App: React.FC = () => 'hello'
    

    解决方法:

    const App: React.FC<{}> = props => props.children as any
    const App: React.FC<{}> = () => [1, 2, 3] as any
    const App: React.FC<{}> = () => 'hello' as any
    // 或者
    const App: React.FC<{}> = props => (props.children as unknown) as JSX.Element
    const App: React.FC<{}> = () => ([1, 2, 3] as unknown) as JSX.Element
    const App: React.FC<{}> = () => ('hello' as unknown) as JSX.Element
    

    如果出现类型不兼容问题,建议使用以下两种方式:

  • PropsWithChildren
  • 这种方式可以为你省去频繁定义 children 的类型,自动设置 children 类型为 ReactNode

    type AppProps = React.PropsWithChildren<{ message: string }>
    const App = ({ message, children }: AppProps) => (
        {message}
        {children}
    
  • 直接声明:
  • type AppProps = {
      message: string
      children?: React.ReactNode
    const App = ({ message, children }: AppProps) => (
        {message}
        {children}
    

    useState<T>

    默认情况下会为你根据你设置的初始值自动进行推导,但是如果初始值为 null 需要显示的声明类型

    type User = {
      name: string
      age: number
    const [user, setUser] = React.useState<User | null>(null)
    

    useRef<T>

    当初始值为 null 时,有两种创建方式:

    const ref1 = React.useRef<HTMLInputElement>(null)
    const ref2 = React.useRef<HTMLInputElement | null>(null)
    

    这两种的区别在于:

  • 第一种方式的 ref1.current 是只读的(read-only),并且可以传递给内置的 ref 属性,绑定 DOM 元素 ;

  • 第二种方式的 ref2.current 是可变的(类似于声明类的成员变量)

  • 这两种方式在使用时,都需要对类型进行检查:

    const onButtonClick = () => {
      ref1.current?.focus()
      ref2.current?.focus()
    

    在某种情况下,可以省去类型检查,通过添加 ! 断言,不推荐

    // Bad
    function MyComponent() {
      const ref1 = React.useRef<HTMLDivElement>(null!)
      React.useEffect(() => {
        //  不需要做类型检查,需要人为保证ref1.current.focus一定存在
        doSomethingWith(ref1.current.focus())
      return <div ref={ref1}> etc </div>
    

    useEffect

    返回值只能是函数或者是 undefined

    useMemo<T> / useCallback<T>

    useMemo 和 useCallback 都可以直接从它们返回的值中推断出它们的类型

    useCallback 的参数必须制定类型,否则 ts 不会报错,默认指定为 any

    const value = 10
    // 自动推断返回值为 number
    const result = React.useMemo(() => value * 2, [value])
    // 自动推断 (value: number) => number
    const multiply = React.useCallback((value: number) => value * multiplier, [
      multiplier,
    

    同时也支持传入泛型, useMemo 的泛型指定了返回值类型,useCallback 的泛型指定了参数类型

    // 也可以显式的指定返回值类型,返回值不一致会报错
    const result = React.useMemo<string>(() => 2, [])
    // 类型“() => number”的参数不能赋给类型“() => string”的参数。
    const handleChange = React.useCallback<
      React.ChangeEventHandler<HTMLInputElement>
    >(evt => {
      console.log(evt.target.value)
    }, [])
    

    自定义 Hook

    需要注意,自定义 Hook 的返回值如果是数组类型,TS 会自动推导为 Union 类型,而我们实际需要的是数组里里每一项的具体类型,需要手动添加 const 断言 进行处理

    function useLoading() {
      const [isLoading, setState] = React.useState(false)
      const load = (aPromise: Promise<any>) => {
        setState(true)
        return aPromise.then(() => setState(false))
      // 实际需要: [boolean, typeof load] 类型
      // 而不是自动推导的:(boolean | typeof load)[]
      return [isLoading, load] as const
    

    如果使用 const 断言遇到问题,也可以直接定义返回类型:

    export function useLoading(): [
      boolean,
      (aPromise: Promise<any>) => Promise<any>
      const [isLoading, setState] = React.useState(false)
      const load = (aPromise: Promise<any>) => {
        setState(true)
        return aPromise.then(() => setState(false))
      return [isLoading, load]
    

    如果有大量的自定义 Hook 需要处理,这里有一个方便的工具方法可以处理 tuple 返回值:

    function tuplify<T extends any[]>(...elements: T) {
      return elements
    function useLoading() {
      const [isLoading, setState] = React.useState(false)
      const load = (aPromise: Promise<any>) => {
        setState(true)
        return aPromise.then(() => setState(false))
      // (boolean | typeof load)[]
      return [isLoading, load]
    function useTupleLoading() {
      const [isLoading, setState] = React.useState(false)
      const load = (aPromise: Promise<any>) => {
        setState(true)
        return aPromise.then(() => setState(false))
      // [boolean, typeof load]
      return tuplify(isLoading, load)
    

    默认属性 defaultProps

    在我们进行迁移的时候,我们可以通过自定义组件 props 类型的必选或者可选来规定参数是否必须传,而对于 angular 中有初始值的@Input 属性,我们可以使用 React 组件的默认值来为其初始化

    但是大部分文章都不推荐使用 defaultProps 来进行初始化,推荐使用默认参数值来代替默认属性

    但是这种方式对于属性特别多的时候又很鸡肋

    type GreetProps = { age?: number }
    const Greet = ({ age = 21 }: GreetProps) => {
      /* ... */
    

    Types or Interfaces

    implements 与 extends 静态操作,不允许存在一种或另一种实现的情况,所以不支持使用联合类型:

    使用 Type 还是 Interface?

    有几种常用规则:

  • 在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口

  • 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强

  • interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:

  • type 类型不能二次编辑,而 interface 可以随时扩展
  • interface Animal {
      name: string
    // 可以继续在原有属性基础上,添加新属性:color
    interface Animal {
      color: string
    /********************************/
    type Animal = {
      name: string
    // type类型不支持属性扩展
    // Error: Duplicate identifier 'Animal'
    type Animal = {
      color: string
    

    获取未导出的 Type

    某些场景下我们在引入第三方的库时会发现想要使用的组件并没有导出我们需要的组件参数类型或者返回值类型,这时候我们可以通过 ComponentProps/ ReturnType 来获取到想要的类型。

    // 获取参数类型 import { Button } from 'library' // 但是未导出props type type ButtonProps = React.ComponentProps<typeof Button> // 获取props type AlertButtonProps = Omit<ButtonProps, 'onClick'> // 去除onClick const AlertButton: React.FC<AlertButtonProps> = props => ( <Button onClick={() => alert('hello')} {...props} /> // 获取返回值类型 function foo() { return { baz: 1 } type FooReturn = ReturnType<typeof foo> // { baz: number }

    Props

    通常我们使用 type 来定义 Props,为了提高可维护性和代码可读性,在日常的开发过程中我们希望可以添加清晰的注释。

    现在有这样一个 type

    type OtherProps = {
      name: string
      color: string
    

    增加相对详细的注释,使用时会更清晰,需要注意,注释需要使用 /**/ , // 无法被 vscode 识别

    // Great
     * @param color color
     * @param children children
     * @param onClick onClick
    type Props = {
      /** color */
      color?: string
      /** children */
      children: React.ReactNode
      /** onClick */
      onClick: () => void
    // type Props
    // @param color — color
    // @param children — children
    // @param onClick — onClick
    const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
      return (
        <button style={{ backgroundColor: color }} onClick={onClick}>
          {children}
        </button>
    

    常用的 React 属性类型

    export declare interface AppBetterProps {
      children: React.ReactNode // 一般情况下推荐使用,支持所有类型 Great
      functionChildren: (name: string) => React.ReactNode
      style?: React.CSSProperties // 传递style对象
      onChange?: React.FormEventHandler<HTMLInputElement>
    

    Forms and Events

    onChange

    change 事件,有两个定义参数类型的方法。

  • 第一种方法使用推断的方法签名(例如:React.FormEvent <HTMLInputElement> :void)
  • import * as React from 'react'
    type changeFn = (e: React.FormEvent<HTMLInputElement>) => void
    const App: React.FC = () => {
      const [state, setState] = React.useState('')
      const onChange: changeFn = e => {
        setState(e.currentTarget.value)
      return (
          <input type="text" value={state} onChange={onChange} />
    
  • 第二种方法强制使用 @types / react 提供的委托类型,两种方法均可。
  • import * as React from 'react'
    const App: React.FC = () => {
      const [state, setState] = React.useState('')
      const onChange: React.ChangeEventHandler<HTMLInputElement> = e => {
        setState(e.currentTarget.value)
      return (
          <input type="text" value={state} onChange={onChange} />
    

    onSubmit

    如果不太关心事件的类型,可以直接使用 React.SyntheticEvent,如果目标表单有想要访问的自定义命名输入,可以使用类型扩展

    import * as React from 'react'
    const App: React.FC = () => {
      const onSubmit = (e: React.SyntheticEvent) => {
        e.preventDefault()
        const target = e.target as typeof e.target & {
          password: { value: string }
        } // 类型扩展
        const password = target.password.value
      return (
        <form onSubmit={onSubmit}>
            <label>
              Password:
              <input type="password" name="password" />
            </label>
            <input type="submit" value="Log in" />
        </form>
    

    不要在 type 或 interface 中使用函数声明

    保持一致性,类型/接口的所有成员都通过相同的语法定义。

    --strictFunctionTypes 在比较函数类型时强制执行更严格的类型检查

    我们在进行事件注册时经常会在事件处理函数中使用 event 事件对象,例如当使用鼠标事件时我们通过 clientX、clientY 去获取指针的坐标。

    大家可能会想到直接把 event 设置为 any 类型,但是这样就失去了我们对代码进行静态检查的意义。

    幸运的是 React 的声明文件提供了 Event 对象的类型声明。

    Event 事件对象类型

    ClipboardEvent<T = Element> 剪切板事件对象

    DragEvent<T =Element> 拖拽事件对象

    ChangeEvent<T = Element> Change 事件对象

    KeyboardEvent<T = Element> 键盘事件对象

    MouseEvent<T = Element> 鼠标事件对象

    TouchEvent<T = Element> 触摸事件对象

    WheelEvent<T = Element> 滚轮时间对象

    AnimationEvent<T = Element> 动画事件对象

    TransitionEvent<T = Element> 过渡事件对象

    事件处理函数类型

    当我们定义事件处理函数时有没有更方便定义其函数类型的方式呢?答案是使用 React 声明文件所提供的 EventHandler 类型别名,通过不同事件的 EventHandler 的类型别名来定义事件处理函数的类型

    type EventHandler<E extends React.SyntheticEvent<any>> = {
      bivarianceHack(event: E): void
    }['bivarianceHack']
    type ReactEventHandler<T = Element> = EventHandler<React.SyntheticEvent<T>>
    type ClipboardEventHandler<T = Element> = EventHandler<React.ClipboardEvent<T>>
    type DragEventHandler<T = Element> = EventHandler<React.DragEvent<T>>
    type FocusEventHandler<T = Element> = EventHandler<React.FocusEvent<T>>
    type FormEventHandler<T = Element> = EventHandler<React.FormEvent<T>>
    type ChangeEventHandler<T = Element> = EventHandler<React.ChangeEvent<T>>
    type KeyboardEventHandler<T = Element> = EventHandler<React.KeyboardEvent<T>>
    type MouseEventHandler<T = Element> = EventHandler<React.MouseEvent<T>>
    type TouchEventHandler<T = Element> = EventHandler<React.TouchEvent<T>>
    type PointerEventHandler<T = Element> = EventHandler<React.PointerEvent<T>>
    type UIEventHandler<T = Element> = EventHandler<React.UIEvent<T>>
    type WheelEventHandler<T = Element> = EventHandler<React.WheelEvent<T>>
    type AnimationEventHandler<T = Element> = EventHandler<React.AnimationEvent<T>>
    type TransitionEventHandler<T = Element> = EventHandler<
      React.TransitionEvent<T>
    

    Promise 类型

    在做异步操作时我们经常使用 async 函数,函数调用时会 return 一个 Promise 对象,可以使用 then 方法添加回调函数。Promise<T> 是一个泛型类型,T 泛型变量用于确定 then 方法时接收的第一个回调函数的参数类型。

    泛型参数的组件

    type Props<T> = {
      name: T
      name2?: T
    const TestC: <T>(props: Props<T>) => React.ReactElement = ({ name, name2 }) => {
      return (
        <div className="test-b">
          TestB--{name}
          {name2}
    const TestD = () => {
      return (
          <TestC<string> name="123" />
    

    什么时候使用泛型

  • 需要作用到很多类型的时候

  • 需要被用到很多地方的时候,比如常用的工具泛型 Partial

  • 如果需要深 Partial 我们可以通过泛型递归来实现

    type DeepPartial<T> = T extends Function
      : T extends object
      ? { [P in keyof T]?: DeepPartial<T[P]> }
    type PartialedWindow = DeepPartial<Window>
    
  •