关于React Hook使用TypeScript遇到的问题记录

906 阅读4分钟

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」。

最近使用 React + Hook + TS 写了一个练手的demo,练习了 React + Hook 的 TypeScript 写法。本文主要记录自己在写demo过程中,遇到关于 TypeScript 写法上的坑。

demo地址:「ts-todo」

1. 创建 TS 项目

使用create-react-app创建React项目,应该没有什么坑,记得在创建React项目的时候,添加typescript配置即可:

npx create-react-app app-name --template typescript

2. 在 React Hook 中 TS 写法

将所有.js文件全部更名为.ts后缀,所有.jsx文件更名为.tsx,若还使用 JavaScript 的写法的话,项目还是能运行的,但配置了 TypeScript 还写 JavaScript 就达不到练手的目的了!

在写demo的过程中,遇到了如下几个坑,经过查找后得到了解决,特此记录。

1. 函数组件

既然要用 React Hook,就需要写函数组件,我们需要告诉 TypeScript,这个函数是个 React 组件,在 JavaScript 中,通常这样定义一个函数组件:

const App = () => {
    return <>App</>;
};

配置了 TypeScript,就需要加上函数类型。React 提供了React.FunctionComponent的类型,意思是函数组件,我们可以直接简写为React.FC,如下代码所示:

const App: React.FC = () => {
    return <>App</>;
};

2. 类型声明举例

一些简单的类型声明,比如numberstringboolean类型的,只需在声明的变量后面加上:[类型]即可。

比如,定义一个number类型的ageboolean类型的isDone

let age: number = 37;
let isDone: boolean = false;

在写demo中,我使用到一个对象数组,如下结构:

[
    { id: 'xxxx', content: 'xxxxxxx', isDone: false },
    { id: 'xxxx', content: 'xxxxxxx', isDone: false },
    { id: 'xxxx', content: 'xxxxxxx', isDone: false },
]

那么一般的array类型已经满足不了了,这时就需要再定义一个interface

interface TaskObj {
    id: string;
    content: string;
    isDone: boolean;
}

注意:接口名通常是大写字母开头。

这个interface表示,定义的对象需要同时满足以下条件:

  • id属性,且类型为string
  • content属性,且类型为string
  • isDone属性,且类型为boolean

TypeScript并不会检查属性的顺序,只需相应的属性存在,并且类型也满足即可。

这时候定义单个对象,就可以使用刚刚定义的interface了:

const obj: TaskObj = { id: 'xxxx', content: 'xxxxxxx', isDone: false };

但如果需要定义上述的对象数组,需要这样写:

const allTasks: TaskObj[] = [
    { id: 'xxxx', content: 'xxxxxxx', isDone: false },
    { id: 'xxxx', content: 'xxxxxxx', isDone: false },
    { id: 'xxxx', content: 'xxxxxxx', isDone: false },
];

TaskObj[]表示该类型是一个数组,数组里的元素需要满足TaskObj接口。

3. useState

对于存储string类型的state,在useState右边加上<string>即可:

const [input, setInput] = useState<string>('');

实际上,useState<string>('')已经给了初始值'',那么 TypeScript 会自动类型判断,即使不写<string>也可以。

如果要存储的数据结构如下:

[
    { id: 'xxxx', content: 'xxxxxxx', isDone: false },
    { id: 'xxxx', content: 'xxxxxxx', isDone: false },
    { id: 'xxxx', content: 'xxxxxxx', isDone: false },
]

那么还是需要上文提到的interface

interface TaskObj {
    id: string;
    content: string;
    isDone: boolean;
}

const [task, setTask] = useState<TaskObj[]>([]);

4. 父组件给子组件传参

在 JavaScript 中,父组件给子组件传递参数,直接子组件上写,子组件通过props接收即可:

<Doing doing={doing} setTask={setTask} />

但是 TypeScript 若直接这么写会报错,原因是子组件并没有满足传递参数的接口,需要自己定义接口。

如上代码,在子组件Doing外,需要定一个满足传递参数的接口:

interface DoingProps {
    doing: TaskObj[];
    setTask: Function;
}

const Doing: React.FC<DoingProps> = ({ doing, setTask }) => {
	...
}

如上代码,定义了一个DoingProps接口,并且在子组件的React.FC后添加<DoingProps>,表明该组件接收的参数需要满足如下条件:

  • doing属性,且类型为数组,数组的元素满足TaskObj接口。
  • setTask属性,且类型是一个函数。

这样,父组件给子组件传参的过程就用 TypeScript 实现了。

5. 类型断言

在写demo中,需要拿到一个输入框的DOM节点,写了如下代码:

const inputNode: HTMLInputElement = document.getElementById(`${id}`)

TypeScript 却提示有如下错误:

这时候只需加上类型断言即可,明确告诉编译器变量的类型:

const inputNode: HTMLInputElement = document.getElementById(`${id}`) as HTMLInputElement;

6. 键盘事件

需要判断按下的按键,这时候需要键盘事件对象,用 TypeScript 来写,需要先将键盘事件KeyboardEvent引入:

import { KeyboardEvent } from 'react';

传递事件对象参数时,需要指明为键盘事件:

<input 
    type="text" 
    defaultValue="{obj.content}" 
    onKeyDown={(e: KeyboardEvent) => updateTask(e, obj.id, obj.content)}
/>

updateTask函数定义如下:

const updateTask = (e: KeyboardEvent, id: string, oldContent: string) => {
	...
}