TypeScript 系列:React 非常有用的类型工具 React.ComponentProps

324 阅读3分钟

📖 TL;DR

有时候我们想复用某个组件的类型,这时候如果组件类型没有被 export 那我们似乎就没法做到了,难道只能被辱使用 any?此时 React.ComponentProps 就派上用场了。

📀 示例

image.png

假设我们在某个组件库的 PieChart 组件之上封装了一个新组件 PieChartWithTitle,目的是给这个组件加上 title,其他属性都直接透传如 data,代码如下:

import { PieChart } from 'third-party-ui';

const PieChartWithTitle = ({ title, data }: { title: string; data: any /* 👈🏻 可恶的 any */ }) => (
  <div style={{ ... }}>
    <h4 style={{ ... }}>
      {title}
    </h4>

    // 其他字段透传
    <PieChart data={data} />
  </div>
);

对自己要求比较严格的同学会发现,这里有个可恶的 any 如鲠在喉如芒在背,不拔不快。

那怎么替换呢?这个 data 是传给 PieChart 的,我们要是能获取其 Props 类型就好了,但该组件并未导出 IPieChartProps 😭。

🥳 React.ComponentProps to the Rescue

我们可以使用 React.ComponentProps

type IParChartData = React.ComponentProps<typeof PieChart>['data'];

const PieChartWithTitle = ({ title, data }: { title: string; data: IParChartData }) => (

我们通过强大的 React.ComponentProps 传入组件类型输出组件属性的类型,然后通过类似 JS 的取值语法即可获取目标字段类型:IPieChartProps['data']

注意:

  • PieChart 前必须加 typeof 否则会报错,因为 React.ComponentProps 接受一个类型而非值。
  • 其次 React.ComponentType 是获取组件类型,这里并不适用,当然勉强用也行如下:
// 可读性差不建议
type IParChartData = Parameters<
  React.ComponentProps<React.ComponentType<typeof PieChart>>
>[0]['data'];

👩‍💻 如何实现 React.ComponentType

就如获取函数入参一样 Parameters 我们可以用 infer 来推导 props 类型。

假设有如何函数,想获取其参数 bar 和 baz 的类型:

function foo(bar: string, baz?: number) {}
type IBar = Parameters<typeof foo>[0] // type IBar = string

type IBaz = Parameters<typeof foo>[1] // type IBaz = number | undefined

这是 TS 内置类型工具,我们可以通过实现来一窥其原理:

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
  ? P
  : never;

注意关键字 infer

接下来我们实现稍微难一点的 ComponentProps。首先观察下结构: type P = typeof PieChart; hover P 可“展开”其具体类型:type P = React.FunctionComponent<IProps>,而我们刚好要的是 IProps,故同样可通过 infer 来精准定位 infer 其 prop 类型。

type ComponentProps<C> = C extends React.ComponentType<infer P> ? P : never;

是不是很简单。我们和官方实现对比看看:

/**
 * NOTE: prefer ComponentPropsWithRef, if the ref is forwarded,
 * or ComponentPropsWithoutRef when refs are not supported.
 */
type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = T extends
    JSXElementConstructor<infer P> ? P
    : T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T]
    : {};

从官方的实现看出不只是获取组件还可以获取内置类型的 props 类型。我们试试:

// 所有属性
type IInput = React.ComponentProps<'input'>;
// 等价于
type IInput = JSX.IntrinsicElements['input']; // 注意是 `[]`

// 二者等价于
type IInput = React.ClassAttributes<HTMLInputElement> & React.InputHTMLAttributes<HTMLInputElement>
    
// 单个属性
type IInputOnChange = React.ComponentProps<'input'>['onChange'];

确实如此。

🌟 还有哪些我们经常使用的类型

  • React.CSSProperties使用频率高 用作 style 的类型,确保不会写错 CSS 属性,非常推荐使用。
  • React.ReactNode使用频率高 接受任意类型,可以是 primitive 类型也可以传入组件,一般当做 children 的类型。
  • React.ReactElement使用频率高 如果控制属性只能传入组件,则可使用。
  • JSX.IntrinsicElements:获取标签 prop 类型:type ButtonProps = JSX.IntrinsicElements["button"]
  • React.FooAttributes<FooElement> 获取某个原生类型的 React 属性,一般用来封装既有组件以便满足 SOLID 的 LSP 原则
    • React.SVGAttributes<SVGElement>:获取 SVG 元素的所有属性
    • React.InputHTMLAttributes<HTMLInputElement>:获取 Input 元素的所有属性
    • ……
  • React.FC使用频率高 函数式组件,隐含可选的 children 属性 children?: React.ReactNode

虽然 React.FC 用法很简单,但是不推荐使用,应该使用更明确的定义。

Not good

type IProps = { title: string }
const Component: React.FC<IProps> = ({ title }) => {}

Good:采用普通函数的写法

显示表明不接受 children,防止调用者产生这样的疑惑“为何接受 children 但传入却没用”。

const Component = ({ title }: { title: string }) => {}

// 或者显示表明一定要传入 children
type IProps2 = {
  title: string;
  
  // 或 React.ReactElement 或 NonNullable<React.ReactNode> 是业务情况而定
  children: React.ReactNode; 
};

const Component2 = ({ title, children }: IProps2) => {}

📚 更多阅读

附录:Antd 显示导出组件类型如 FormProps 方便使用

import type { FormProps } from 'antd';
import { Button, Checkbox, Form, Input } from 'antd';

type FieldType = {
  username?: string;
  password?: string;
  remember?: string;
};

const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
  console.log('Success:', values);
};

来自:ant-design.antgroup.com/components/…