📖 TL;DR
有时候我们想复用某个组件的类型,这时候如果组件类型没有被 export 那我们似乎就没法做到了,难道只能被辱使用 any?此时 React.ComponentProps 就派上用场了。
📀 示例
假设我们在某个组件库的 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) => {}
📚 更多阅读
- ComponentProps: React's Most Useful Type Helper —— 来自著名 TS 专家 Matt Pocock
- TypeScript + React: Why I don't use React.FC
附录: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);
};