巧用 React.ComponentProps 提取组件属性类型

277 阅读3分钟

在 React + TypeScript 开发中,有时我们需要使用组件库中某些类型,但组件库并未导出这些类型。最近我就遇到一个这样的场景 - 在XTaro的开发中,需要获取某个组件 onInput 事件对应的 event 类型,但组件库没有导出这个类型。这促使我深入研究了 React.ComponentProps 这个实用的工具类型,在此分享给大家。

从实际问题说起

假设我们在使用一个第三方组件库中的 Input 组件:

import { Input } from 'some-ui-library'

// 我们需要获取 onInput 事件的类型
const MyComponent = () => {
  // 这里不确定 event 的具体类型
  const handleInput = (event: unknown) => {
    console.log(event.target.value) // TypeScript 报错:Object is of type 'unknown'
  }

  return <Input onInput={handleInput} />
}

这时 React.ComponentProps 就派上用场了:

import { ComponentProps } from 'react'
import { Input } from 'some-ui-library'

// 提取 Input 组件的 onInput 属性类型
type InputEvent = ComponentProps<typeof Input>['onInput'] extends 
  (event: infer E) => void ? E : never

const MyComponent = () => {
  // 现在 event 有了正确的类型
  const handleInput = (event: InputEvent) => {
    console.log(event.target.value) // TypeScript 不再报错
  }

  return <Input onInput={handleInput} />
}

ComponentProps 的强大之处

1. 提取原生 HTML 元素的属性类型

type DivProps = ComponentProps<'div'>
type ButtonProps = ComponentProps<'button'>

// 创建带有原生属性的自定义组件
const CustomButton = (props: ComponentProps<'button'> & { theme: 'light' | 'dark' }) => {
  const { theme, ...buttonProps } = props
  return (
    <button 
      {...buttonProps} 
      className={`${props.className || ''} theme-${theme}`}
    />
  )
}

2. 提取第三方组件的属性类型

import { Table } from 'antd'

// 提取表格列的配置类型
type ColumnConfig = ComponentProps<typeof Table>['columns'][number]

// 提取分页配置的类型
type PaginationConfig = NonNullable<ComponentProps<typeof Table>['pagination']>

const MyTable = () => {
  const columns: ColumnConfig[] = [
    {
      title: '姓名',
      dataIndex: 'name',
      sorter: true
    }
  ]

  const pagination: PaginationConfig = {
    current: 1,
    pageSize: 10,
    total: 100
  }

  return <Table columns={columns} pagination={pagination} />
}

3. 创建类型安全的高阶组件

import { ComponentProps, ComponentType } from 'react'

// 创建一个添加主题功能的高阶组件
function withTheme<T extends ComponentProps<any>>(
  WrappedComponent: ComponentType<T>
) {
  return (props: T & { theme?: 'light' | 'dark' }) => {
    const { theme = 'light', ...componentProps } = props
    
    return (
      <div className={`theme-${theme}`}>
        <WrappedComponent {...(componentProps as T)} />
      </div>
    )
  }
}

// 使用高阶组件
const ThemedButton = withTheme(CustomButton)

4. 提取事件处理器的参数类型

import { Select } from 'some-ui-library'

// 提取 onChange 事件处理器的参数类型
type SelectChangeHandler = ComponentProps<typeof Select>['onChange']
type SelectValue = Parameters<NonNullable<SelectChangeHandler>>[0]

const MySelect = () => {
  const handleChange = (value: SelectValue) => {
    console.log('Selected:', value)
  }

  return <Select onChange={handleChange} />
}

5. 组合多个组件的属性类型

import { Input, Select } from 'some-ui-library'

// 组合 Input 和 Select 的某些属性
type ComboProps = Pick<ComponentProps<typeof Input>, 'placeholder' | 'disabled'> &
  Pick<ComponentProps<typeof Select>, 'options'>

const ComboBox = (props: ComboProps) => {
  const { placeholder, disabled, options } = props
  
  return (
    <div className="combo-box">
      <Input placeholder={placeholder} disabled={disabled} />
      <Select options={options} disabled={disabled} />
    </div>
  )
}

最佳实践和注意事项

  1. 使用 NonNullable 处理可能为空的属性:
type DefiniteHandler = NonNullable<ComponentProps<typeof Input>['onChange']>
  1. 使用 PickOmit 选择性地提取或排除属性:
type PickedProps = Pick<ComponentProps<typeof Component>, 'onClick' | 'className'>
type OmittedProps = Omit<ComponentProps<typeof Component>, 'ref'>
  1. 处理泛型组件时注意类型参数:
type TableProps<T> = ComponentProps<typeof Table<T>>
  1. 提取嵌套属性类型时使用类型索引:
type OptionType = ComponentProps<typeof Select>['options'][number]

总结

React.ComponentProps 是一个强大的工具类型,它可以帮助我们:

  • 提取未导出的组件属性类型
  • 复用现有组件的类型定义
  • 创建类型安全的组件封装
  • 处理事件处理器的类型
  • 组合多个组件的属性类型

在实际开发中,合理使用 ComponentProps 可以大大提高代码的类型安全性和开发效率。特别是在使用第三方组件库时,它能帮助我们获取那些未导出的类型定义,是一个不可或缺的工具。

(小声逼逼:要不是它,我可能又要忍不住开启AnyScript大法了)