跳到主要内容

React组件如何写TypeScript类型

现在写 React 组件都是基于 TypeScript,所以如何给组件写类型也是很重要的。

这节我们就来学下 React 组件如何写 TypeScript 类型。

用 cra 创建个项目:

npx create-react-app --template typescript react-ts

我们平时用的类型在 @types/react 这个包里,cra 已经帮我们引入了。

JSX 的类型

在 App.tsx 里开始练习 TypeScript 类型:

interface AaaProps {
name: string;
}

function Aaa(props: AaaProps) {
return <div>aaa, {props.name}</div>;
}

function App() {
return (
<div>
<Aaa name="guang"></Aaa>
</div>
);
}

export default App;

其实组件我们一般不写返回值类型,就用默认推导出来的。

React 函数组件默认返回值就是 JSX.Element。

我们看下 JSX.Element 的类型定义:

const content: JSX.Element = <div>aaa</div>;

可以看到它就是 React.ReactElement。

也就是说,如果你想描述一个 jsx 类型,就用 React.ReactElement 就好了。

比如 Aaa 组件有一个 content 的 props,类型为 ReactElement:

这样就只能传入 JSX。

跑一下:

npm run start

ReactElement 就是 jsx 类型,但如果你传入 null、number 等就报错了:

那如果有的时候就是 number、null 呢?

换成 ReactNode 就好了:

看下它的类型定义:

ReactNode 包含 ReactElement、或者 number、string、null、boolean 等可以写在 JSX 里的类型。

这三个类型的关系 ReactNode > ReactElement > JSX.Element。

所以,一般情况下,如果你想描述一个参数接收 JSX 类型,就用 ReactNode 就行。

函数组件的类型

前面的函数组件,我们都没明确定义类型:

其实它的类型是 FunctionComponent:

const Aaa: React.FunctionComponent<AaaProps> = (props) => {
return (
<div>
aaa, {props.name}
{props.content}
</div>
);
};

看下它的类型定义:

可以看到,FC 和 FunctionComponent 等价,参数是 Props,返回值是 ReactNode。

而且函数组件还可以写几个可选属性,这些用到了再说。

hook 的类型

接下来过一下 hook 的类型:

useState

先从 useState 开始:

一般用推导出的类型就行:

也可以手动声明类型:

useEffect 和 useLayoutEffect 这种没有类型参数的就不说了。

useRef

useRef 我们知道,可以保存 dom 引用或者其他内容。

所以它的类型也有两种。

保存 dom 引用的时候,参数需要传个 null:

不然会报错:

而保存别的内容的时候,不能传 null,不然也会报错,说是 current 只读:

为什么呢?

看下类型就知道了:

当你传入 null 的时候,返回的是 RefObject,它的 current 是只读的:

这很合理,因为保存的 dom 引用肯定不能改呀。

而不传 null 的时候,返回的 MutableRefObject,它的 current 就可以改了:

因为 ref 既可以保存 dom 引用,又可以保存其他数据,而保存 dom 引用又要加上 readonly,所以才用 null 做了个区分。

传 null 就是 dom 引用,返回 RefObject,不传就是其他数据,返回 MutableRefObject。

所以,这就是一种约定,知道传 null 和不传 null 的区别就行了。

useImperativeHandle

我们前面写过 forwardRef + useImperativeHandle 的例子,是这样的:

import { useRef } from "react";
import { useEffect } from "react";
import React from "react";
import { useImperativeHandle } from "react";

interface GuangProps {
name: string;
}

interface GuangRef {
aaa: () => void;
}

const Guang: React.ForwardRefRenderFunction<GuangRef, GuangProps> = (
props,
ref
) => {
const inputRef = useRef<HTMLInputElement>(null);

useImperativeHandle(ref, () => {
return {
aaa() {
inputRef.current?.focus();
},
};
}, [inputRef]);

return (
<div>
<input ref={inputRef}></input>
<div>{props.name}</div>
</div>
);
};

const WrapedGuang = React.forwardRef(Guang);

function App() {
const ref = useRef<GuangRef>(null);

useEffect(() => {
console.log("ref", ref.current);
ref.current?.aaa();
}, []);

return (
<div className="App">
<WrapedGuang name="guang" ref={ref} />
</div>
);
}

export default App;

forwardRef 包裹的组件会额外传入 ref 参数,所以它不是 FunctionComponent 类型,而是专门的 ForwardRefRenderFunction 类型。

它有两个类型参数,第一个是 ref 内容的类型,第二个是 props 的类型:

其实 forwardRef 也是这两个类型参数,所以写在 forwardRef 上也行:

import { useRef } from "react";
import { useEffect } from "react";
import React from "react";
import { useImperativeHandle } from "react";

interface GuangProps {
name: string;
}

interface GuangRef {
aaa: () => void;
}

const WrapedGuang = React.forwardRef<GuangRef, GuangProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);

useImperativeHandle(ref, () => {
return {
aaa() {
inputRef.current?.focus();
},
};
}, [inputRef]);

return (
<div>
<input ref={inputRef}></input>
<div>{props.name}</div>
</div>
);
});

useImperativeHanlde 可以有两个类型参数,一个是 ref 内容的类型,一个是 ref 内容扩展后的类型。

useImperativeHanlde 传入的函数的返回值就要求满足第二个类型参数的类型

不过一般没必要写,因为传进来的 ref 就已经是有类型的了,直接用默认推导的就行。

useReducer

useReducer 可以传一个类型参数也可以传两个:

当传一个的时候,是 Reducer<xx,yy> 类型,xx 是 state 的类型,yy 是 action 的类型。

当传了第二个的时候,就是传入的初始化函数参数的类型。

其余 hook

剩下的 hook 的类型比较简单,我们快速过一遍:

useCallback 的类型参数是传入的函数的类型:

useMemo 的类型参数是传入的函数的返回值类型:

useContext 的类型参数是 Context 内容的类型:

当然,这些都没必要手动声明,用默认推导的就行。

再就是 memo:

它可以直接用包裹的函数组件的参数类型:

也可以在类型参数里声明:

参数类型

回过头来,我们再来看传入组件的 props 的类型。

PropsWithChildren

前面讲过,jsx 类型用 ReactNode,比如这里的 content 参数:

如果你不想通过参数传入内容,可以在 children 里:

这时候就要声明 children 的类型为 ReactNode:

import React, { ReactNode } from "react";

interface CccProps {
content: ReactNode;
children: ReactNode;
}

function Ccc(props: CccProps) {
return (
<div>
ccc,{props.content}
{props.children}
</div>
);
}

function App() {
return (
<div>
<Ccc content={<div>666</div>}>
<button>7777</button>
</Ccc>
</div>
);
}

export default App;

但其实没有必要自己写,传 children 这种情况太常见了,React 提供了相关类型:

type CccProps = PropsWithChildren<{
content: ReactNode;
}>;

看下它的类型定义:

就是给 Props 加了一个 children 属性。

CSSProperties

有时候组件可以通过 props 传入一些 css 的值,这时候怎么写类型呢?

用 CSSProperties。

比如加一个 color 参数:

或者加一个 styles 参数:

可以看到,提示出了 css 的样式名,以及可用的值:

import React, { CSSProperties, PropsWithChildren, ReactNode } from "react";

type CccProps = PropsWithChildren<{
content: ReactNode;
color: CSSProperties["color"];
styles: CSSProperties;
}>;

function Ccc(props: CccProps) {
return (
<div>
ccc,{props.content}
{props.children}
</div>
);
}

function App() {
return (
<div>
<Ccc
content={<div>666</div>}
color="yellow"
styles={{
backgroundColor: "blue",
}}>
<button>7777</button>
</Ccc>
</div>
);
}

export default App;

HTMLAttributes

如果你写的组件希望可以当成普通 html 标签一样用,也就是可以传很多 html 的属性作为参数呢?

那可以继承 HTMLAttributes:

上图中可以看到,提示了很多 html 的属性。

import React, { HTMLAttributes } from "react";

interface CccProps extends HTMLAttributes<HTMLDivElement> {}

function Ccc(props: CccProps) {
return <div>ccc</div>;
}

function App() {
return (
<div>
<Ccc p>
<button>7777</button>
</Ccc>
</div>
);
}

export default App;

那 HTMLAttributes 的类型参数是干嘛的呢?

是其中一些 onClick、onMouseMove 等事件处理函数的类型参数:

当然,继承 HTMLAttributes 只有 html 通用属性,有些属性是某个标签特有的,这时候可以指定 FormHTMLAttributes、AnchorHTMLAttributes 等:

比如 a 标签的属性,会有 href:

ComponentProps

继承 html 标签的属性,前面用的是 HTMLAttributes:

其实也可以用 ComponentProps:

效果一样。

ComponentProps 的类型参数是标签名,比如 a、div、form 这些。

EventHandler

很多时候,组件需要传入一些事件处理函数,比如 clickHandler:

import React, { HTMLAttributes, MouseEventHandler } from "react";

interface CccProps {
clickHandler: MouseEventHandler;
}

function Ccc(props: CccProps) {
return <div onClick={props.clickHandler}>ccc</div>;
}

function App() {
return (
<div>
<Ccc
clickHandler={(e) => {
console.log(e);
}}></Ccc>
</div>
);
}

export default App;

这种参数就要用 xxxEventHandler 的类型,比如 MouseEventHandler、ChangeEventHandler 等,它的类型参数是元素的类型。

或者不用 XxxEventHandler,自己声明一个函数类型也可以:

interface CccProps {
clickHandler: (e: MouseEvent<HTMLDivElement>) => void;
}

案例代码上传了小册仓库

总结

我们过了一遍写 React 组件会用到的类型:

  • ReactNode:JSX 的类型,一般用 ReactNode,但要知道 ReactNode、ReactElement、JSX.Element 的关系

  • FunctionComonent:也可以写 FC,第一个类型参数是 props 的类型

  • useRef 的类型:传入 null 的时候返回的是 RefObj,current 属性只读,用来存 html 元素;不传 null 返回的是 MutableRefObj,current 属性可以修改,用来存普通对象。

  • ForwardRefRenderFunction:第一个类型参数是 ref 的类型,第二个类型参数是 props 的类型。forwardRef 和它类型参数一样,也可以写在 forwardRef 上

  • useReducer:第一个类型参数是 Reducer<data 类型, action 类型>,第二个类型参数是初始化函数的参数类型。

  • PropsWithChildren:可以用来写有 children 的 props

  • CSSProperties: css 样式对象的类型

  • HTMLAttributes:组件可以传入 html 标签的属性,也可以指定具体的 ButtonHTMLAttributes、AnchorHTMLAttributes。

  • ComponentProps:类型参数传入标签名,效果和 HTMLAttributes 一样

  • EventHandler:事件处理器的类型

后面写 React 组件的时候,会大量用到这些 typescript 的类型。