最近使用 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. 类型声明举例
一些简单的类型声明,比如number
、string
、boolean
类型的,只需在声明的变量后面加上:[类型]
即可。
比如,定义一个number
类型的age
和boolean
类型的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) => {
...
}