如何在React中使用TypeScript

在这篇文章中,你将学习如何使用TypeScript和React通过本文的阅读,你将对如何使用TypeScript编写React代码有一个扎实的理解想要观看本教程的视频版本吗?你可以在下方查看视频:目录 *

在这篇文章中,您将学习如何在React中使用TypeScript。

到最后,您将对如何使用TypeScript编写React代码有了扎实的理解。

想观看本教程的视频版本吗?您可以在下面查看视频:

目录

先决条件

要跟随本教程,您需要以下准备:

  • 基本了解如何使用React
  • 基本了解TypeScript代码的编写

入门

要开始使用TypeScript,您首先需要在您的机器上安装TypeScript。您可以通过在终端或命令提示符中执行npm install -g typescript来完成此操作。

现在,我们将使用TypeScript创建一个Vite项目。

npm create vite

执行后,您将被问一些问题。

对于项目名称,请输入react-typescript-demo

对于框架,请选择React,对于变体,请选择TypeScript

1_create_project
使用Vite创建项目

创建项目后,将其在VS Code中打开,并从终端执行以下命令:

cd react-typescript-demonpm install

现在,让我们进行一些代码清理。

删除src/App.css文件,并用以下内容替换src/App.tsx文件的内容:

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

保存文件后,您可能会看到文件中的红色下划线,如下所示:

2_red_error
TypeScript版本错误

如果遇到该错误,只需按下Cmd + Shift + P(Mac)Ctrl + Shift + P(Windows/Linux)打开VS Code命令面板,然后在搜索框中输入TypeScript文本,并选择选项TypeScript: Select TypeScript Version...

3_version_options
VSCode命令面板选项

一旦选择,你将看到如下图所示的在VS Code版本和工作区版本之间进行选择的选项:

4_select_option
选择使用工作区版本

从这些选项中,你需要选择Use Workspace Version选项。一旦选择了该选项,App.tsx文件中的错误就会消失。

现在,打开src/index.css文件,并将其内容替换为以下代码:

:root {  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;  line-height: 1.5;  font-weight: 400;  font-synthesis: none;  text-rendering: optimizeLegibility;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  -webkit-text-size-adjust: 100%;}

现在,通过执行npm run dev命令来启动应用程序。

5_app_started
应用程序已启动

现在,点击显示的URL并访问应用程序。你将在浏览器中看到以下初始界面,其中显示了文本App

701358b4-4bdc-49de-b008-245ef71fc929
正在运行的应用程序

React和TypeScript基础知识

当使用React和TypeScript时,你需要了解的第一件事是文件扩展名。

每个React + TypeScript文件都需要有一个.tsx扩展名。

如果文件不包含任何JSX特定的代码,那么你可以使用.ts扩展名代替.tsx扩展名。

要在React中使用TypeScript创建组件,你可以使用react包中的FC类型,并在组件名称之后使用它。

因此,打开src/App.tsx文件,并用以下内容替换:

import { FC } from 'react';const App: FC = () => {  return <div>App</div>;};export default App;

现在,让我们给这个App组件传递一些props。

src/main.tsx中,将一个title prop传递给App组件,如下所示:

import React from 'react';import ReactDOM from 'react-dom/client';import App from './App.tsx';import './index.css';ReactDOM.createRoot(document.getElementById('root')!).render(  <React.StrictMode>    <App title='TypeScript Demo' />  </React.StrictMode>);

然而,添加了title prop之后,我们现在有一个TypeScript错误,如下所示:

7_prop_error
Prop错误

三种定义Prop类型的方式

我们可以用三种不同的方式来修复上述的TypeScript错误。

  • 使用接口声明类型

错误出现的原因是我们将title prop添加为App组件的必需prop-所以我们需要在App组件内部提到这一点。

打开src/App.tsx文件,并使用以下代码替换其内容:

import { FC } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = () => {  return <div>App</div>;};export default App;

如上所示,我们添加了额外的接口AppProps来指定组件接受的props。我们还在角括号中的FC后面使用了AppProps接口。

开始界面名称以大写字母开头,比如我们的情况是 AppProps,这是一种很好的做法。

现在,通过这个变化,TypeScript错误已经消失,如下所示:

8_no_prop_error
为组件添加属性类型

这是我们指定特定组件接受哪些属性的方式。

  • 使用type进行类型声明

我们也可以使用type关键字来声明属性类型。

所以打开 App.tsx 文件,将下面的代码进行更改:

import { FC } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = () => {  return <div>App</div>;};export default App;

更改为以下代码:

import { FC } from 'react';type AppProps = {  title: string;};const App: FC<AppProps> = () => {  return <div>App</div>;};export default App;

在这里,不再使用interface声明,而是使用了type声明。现在代码将无错误地运行。

你可以根据自己的喜好选择使用哪种方法。我总是喜欢使用接口来声明组件类型。

  • 使用行内类型声明

第三种声明类型的方式是通过定义行内类型,如下所示:

const App = ({ title }: { title: string }) => {  return <div>App</div>;};export default App;

如上所示,我们已经移除了FC的使用,因为它不再需要,而且在解构title属性时,我们定义了其类型。

所以在这三种方式中,你可以选择任何一种。我总是比较喜欢使用带有FC的接口。这样,如果以后想要添加更多属性,代码也不会变得复杂(如果你使用行内类型的话,代码就会变得复杂)。

现在,让我们使用title属性,并在UI上显示出来。

使用以下代码替换App.tsx文件的内容:

import { FC } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = ({ title }) => {  return <h1>{title}</h1>;};export default App;

正如你所见,我们在FC中使用一个接口,然后将title属性解构并在屏幕上显示出来。

现在,打开src/index.css文件,并在其中添加以下CSS:

h1 {  text-align: center;}

如果你在浏览器中查看应用程序,你将看到正确显示了TypeScript Demo文本的标题。

10_title_displayed
应用程序标题正确显示

如何创建一个随机用户列表的应用程序

现在你已经对如何声明组件属性有了基本的了解,让我们创建一个简单的随机用户列表应用程序,它将在屏幕上显示一个含有10个随机用户的列表。

为此,我们将使用Random User Generator API。

我们将使用以下API URL:

https://randomuser.me/api/?results=10

让我们首先安装Axios npm库,以便我们可以使用它进行API调用。

执行以下命令来安装Axios库:

npm install axios

安装完成后,通过执行 npm run dev 命令重新启动应用程序。

现在,用以下内容替换 App.tsx 文件的内容:

import axios from 'axios';import { FC, useEffect } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = ({ title }) => {  useEffect(() => {    const getUsers = async () => {      try {        const { data } = await axios.get(          'https://randomuser.me/api/?results=10'        );        console.log(data);      } catch (error) {        console.log(error);      }    };    getUsers();  }, []);  return <h1>{title}</h1>;};export default App;

如上所示,我们在上面添加了一个 useEffect 钩子,用于调用 API 获取用户列表。

现在,如果在浏览器中打开控制台,你将能够在控制台中看到 API 响应的结果。

11_api_response
API 响应

正如你所见,我们正确地获取了 10 个随机用户的列表,实际用户列表存在响应的 results 属性中。

如何将用户列表存储在状态中

现在,让我们将这些用户存储在状态中,以便在屏幕上显示它们。

App 组件内部,声明一个新的状态,初始值为空数组:

const [users, setUsers ] = useState([]);

并在 API 调用后,在 useEffect 钩子中调用 setUsers 函数将用户存储起来。

因此,你的 App 组件现在会像这样:

import axios from 'axios';import { FC, useEffect, useState } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = ({ title }) => {  const [users, setUsers] = useState([]);  useEffect(() => {    const getUsers = async () => {      try {        const { data } = await axios.get(          'https://randomuser.me/api/?results=10'        );        console.log(data);        setUsers(data.results);      } catch (error) {        console.log(error);      }    };    getUsers();  }, []);  return <h1>{title}</h1>;};export default App;

如你所见,这里我们使用 data.results 的值调用了 setUsers 函数。

如何在界面上显示用户

现在,我们来显示每个用户的姓名和电子邮件。

如果你检查控制台输出,你会发现每个对象都有一个包含用户名和姓氏的 name 属性。因此,我们可以将它们组合在一起以显示完整的姓名。

此外,对于每个用户对象,我们有一个直接的 email 属性,可以用来显示电子邮件。

12_required_properties
探索 API 响应

因此,请用以下内容替换 App.tsx 文件的内容:

import axios from 'axios';import { FC, useEffect, useState } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = ({ title }) => {  const [users, setUsers] = useState([]);  useEffect(() => {    const getUsers = async () => {      try {        const { data } = await axios.get(          'https://randomuser.me/api/?results=10'        );        console.log(data);        setUsers(data.results);      } catch (error) {        console.log(error);      }    };    getUsers();  }, []);  return (    <div>      <h1>{title}</h1>      <ul>        {users.map(({ login, name, email }) => {          return (            <li key={login.uuid}>              <div>                姓名:{name.first} {name.last}              </div>              <div>电子邮件:{email}</div>              <hr />            </li>          );        })}      </ul>    </div>  );};export default App;

正如你所看到的,我们正在使用array map方法循环遍历users数组,并且我们正在使用对象解构来解构单个user对象的loginnameemail属性。此外,我们将用户的姓名和电子邮件显示为无序列表。

但是,你会在文件中看到一些TypeScript错误,如下所示:

13_user_errors
用户属性 TypeScript 错误

这是因为,默认情况下,TypeScript 假定users数组的类型为never[],所以它无法确定users数组包含哪些属性。

这意味着我们需要指定我们正在使用的所有属性以及它们的类型。

所以现在,在AppProps接口之后声明一个新的接口,像这样:

interface Users {  name: {    first: string;    last: string;  };  login: {    uuid: string;  };  email: string;}

在这里,我们指定每个单独的user将是一个具有nameloginemail属性的对象。我们还指定了每个属性的数据类型。

如你所见,从 API 返回的每个user对象还有很多其他属性,如phonelocation等。但是我们只需要指定在代码中使用的那些属性。

现在,将useState中的users数组声明从这个:

const [users, setUsers] = useState([]);

更改为这个:

const [users, setUsers] = useState<Users[]>([]);

在这里,我们指定users是类型为Users的对象数组,这是我们声明的接口。

现在,如果你检查App.tsx文件,你将看不到 TypeScript 错误。

14_no_type_error
已修复的属性类型错误

然后,你将能够在屏幕上看到显示的10个随机用户列表:

15_random_users
屏幕上显示的随机用户列表

正如之前所见,我们声明了像这样的Users接口:

interface Users {  name: {    first: string;    last: string;  };  login: {    uuid: string;  };  email: string;}

但是当你有嵌套属性时,你会看到它写成这样:

interface Name {  first: string;  last: string;}interface Login {  uuid: string;}interface Users {  name: Name;  login: Login;  email: string;}

为每个嵌套属性声明单独的接口的优点是,如果你要在任何其他文件中使用相同的结构,你可以导出上述任何一个接口并在其他文件中重复使用它们(而不是重新声明相同的接口)。

因此,让我们将所有上述接口作为命名导出导出。代码将如下所示:

导出接口名称{  first: string;  last: string;}导出接口登录{  uuid: string;}导出接口用户{  名称: 名称;  登录: 登录;  电子邮件: string;}

就像我之前说的,你也可以在这里使用类型声明,而不是使用接口,这样它看起来像这样:

类型名称 = {  first: string;  last: string;};类型登录 = {  uuid: string;};类型用户 = {  名称: 名称;  登录: 登录;  电子邮件: string;};

如何创建单独的用户组件

当我们使用map方法在屏幕上显示东西时,通常会将显示部分分离到不同的组件中。这样做使得测试变得容易,而且还会使你的组件代码变得更短。

src文件夹中创建一个components文件夹,并在其中创建一个User.tsx文件。然后在该文件中添加以下内容:

const 用户=({ 登录,名称,电子邮件 })=>{  返回(    <li键={登录.uuid}>      <div>        姓名:{名称.first} {名称.last}      </div>      <div>电子邮件:{电子邮件}</div>      <hr />    </li>  );}导出默认用户;

如果保存文件,你会再次看到TypeScript错误。

16_user_component_props_error
用户组件属性声明错误

所以我们需要指定User组件将接收哪些props。我们还需要指定每个props的数据类型。

所以更新后的User.tsx文件将如下所示:

导入{ FC } 从react;导入{ 登录,名称 } 从'../App';接口用户道具{  登录:登录;  名称:名称;  电子邮件:string;}常规人:FC<用户道具>=({ 登录,名称,电子邮件 })=>{  返回(    <li键={登录.uuid}>      <div>        姓名:{名称.first} {名称.last}      </div>      <div>电子邮件:{电子邮件}</div>      <hr />    </li>  );}导出默认用户;

如上所示,我们在上面声明了一个UserProps接口,并在User组件中使用FC进行指定。

另外,请注意我们没有声明namelogin属性的数据类型。相反,我们使用了来自App.tsx文件的导出类型:

导入{ 登录,名称 } 从'../App';

这就是为什么最好为每个嵌套属性声明单独的类型,这样我们可以在其他地方重用它们。

现在,我们可以在App.tsx文件中使用这个User组件。

所以将下面的代码更改为:

{users.map(({ 登录, 名称, 电子邮件 }) => {  返回(    <li键={登录.uuid}>      <div>        姓名:{名称.first} {名称.last}      </div>      <div>电子邮件:{电子邮件}</div>      <hr />    </li>  );})}

改成这段代码:

{users.map(({ 登录, 名称, 电子邮件 }) => {  返回 <用户键={登录.uuid} 名称={名称} 电子邮件={电子邮件} />;})}

如你所知,当使用数组map方法时,我们需要为父元素提供key,在我们的例子中是User。所以我们在使用User组件时添加了key属性。

这意味着我们不需要在 User 组件内部使用 key,所以我们可以删除 User 组件中的 key 和 login 属性。

因此,更新后的 User 组件将如下所示:

import { FC } from 'react';import { Name } from '../App';interface UserProps {  name: Name;  email: string;}const User: FC<UserProps> = ({ name, email }) => {  return (    <li>      <div>        Name: {name.first} {name.last}      </div>      <div>Email: {email}</div>      <hr />    </li>  );};export default User;

如您所见,我们已从接口中删除了 login 属性,并进行了解构。应用程序仍然与之前一样正常工作,没有任何问题,如下所示。

17_working_with_refactor
在用户界面上显示的随机用户列表

如何为类型声明创建单独的文件

请注意,App.tsx 文件因为接口声明而变得很大。通常会有一个单独的文件专门用于声明类型。

因此,在 src 文件夹中创建一个 App.types.ts 文件,并将所有类型声明从 App 组件移动到 App.types.ts 文件中:

export interface AppProps {  title: string;}export interface Name {  first: string;  last: string;}export interface Login {  uuid: string;}export interface Users {  name: Name;  login: Login;  email: string;}

请注意,上述代码还导出了 AppProps 组件。

现在,更新 App.tsx 文件以使用这些类型,如下所示:

import axios from 'axios';import { FC, useEffect, useState } from 'react';import { AppProps, Users } from './App.types';import User from './components/User';const App: FC<AppProps> = ({ title }) => {  const [users, setUsers] = useState<Users[]>([]);    // ...};export default App;

如您所见,我们从 App.types 文件中导入 AppPropsUsers

import { AppProps, Users } from './App.types';

现在,您的 User.tsx 文件将如下所示:

import { FC } from 'react';import { Name } from '../App.types';interface UserProps {  name: Name;  email: string;}const User: FC<UserProps> = ({ name, email }) => {  return (    <li>      <div>        姓名:{name.first} {name.last}      </div>      <div>电子邮件:{email}</div>      <hr />    </li>  );};export default User;

如您所见,我们从 App.types 文件中导入了 Name

import { Name } from '../App.types';

如何显示加载指示器

每当您进行 API 调用来显示内容时,始终最好在 API 调用进行中显示一些加载指示器。

因此,让我们在 App 组件中添加一个新的 isLoading 状态:

const [isLoading, setIsLoading] = useState(false);

如您所见,我们在声明状态时没有指定任何数据类型,像这样:

const [isLoading, setIsLoading] = useState<boolean>(false);

这是因为,当我们分配任何初始值(在我们的例子中是false),TypeScript会自动推断我们将要存储的数据类型 – 在我们的例子中是boolean

当我们声明users状态时,仅通过空数组的初始值[]无法明确存储的内容。所以我们需要像这样指定它的类型:

const [users, setUsers] = useState<Users[]>([]);

现在,将useEffect的代码改为以下代码:

useEffect(() => {  const getUsers = async () => {    try {      setIsLoading(true);      const { data } = await axios.get(        'https://randomuser.me/api/?results=10'      );      console.log(data);      setUsers(data.results);    } catch (error) {      console.log(error);    } finally {      setIsLoading(false);    }  };  getUsers();}, []);

在这里,我们在API调用之前使用setIsLoading赋值为true。在finally块中,我们将其设置回false

无论成功与否,finally块中的代码都将执行。所以无论API调用成功还是失败,我们都需要隐藏加载消息,而我们使用finally块来实现。

现在,我们可以使用isLoading状态值在屏幕上显示加载消息。

h1标签之后、ul标签之前,添加以下代码:

{isLoading && <p>加载中...</p>}

现在,如果您检查应用程序,您将能够在加载用户列表时看到加载消息。

18_loading
加载指示显示

这样用户体验更好。

但是,如果您仔细观察显示的用户,您会发现用户在加载后得到了更改。

这是因为我们正在使用React版本18(您可以从package.json文件中验证)和src/main.tsx文件中的React.StrictMode

在React的18版本中,当我们使用React.StrictMode时,即使没有指定依赖项,每个useEffect钩子也会执行两次。

这只会在开发环境中发生,而在部署应用程序时不会发生。

因此,API调用被执行了两次。由于随机用户API每次调用API时都会返回一组新的随机用户,我们使用useEffect钩子中的setUsers调用将不同的一组用户设置到users数组中。

这就是为什么我们在不刷新页面的情况下看到用户的更改。

如果您不希望在开发过程中出现这种行为,可以从main.tsx文件中删除React.StrictMode

所以将以下代码进行修改:

import React from 'react';import ReactDOM from 'react-dom/client';import App from './App.tsx';import './index.css';ReactDOM.createRoot(document.getElementById('root')!).render(  <React.StrictMode>    <App title='TypeScript Demo' />  </React.StrictMode>);

修改为以下代码:

import ReactDOM from 'react-dom/client';import App from './App.tsx';import './index.css';ReactDOM.createRoot(document.getElementById('root')!).render(  <App title='TypeScript Demo' />);

现在,如果您检查应用程序,您将看到在加载后用户列表不会改变。

如何在按下按钮时加载用户

现在,不再在页面加载时进行API调用,我们将添加一个”显示用户”按钮,并在点击该按钮时进行API调用。

所以在h1标签之后,添加一个新的按钮,如下所示:

<button onClick={handleClick}>显示用户</button>

现在,在App组件中添加handleClick方法,并将所有代码从getUsers函数移动到handleClick方法中:

const handleClick = async () => {  try {    setIsLoading(true);    const { data } = await axios.get('https://randomuser.me/api/?results=10');    console.log(data);    setUsers(data.results);  } catch (error) {    console.log(error);  } finally {    setIsLoading(false);  }};

现在,您可以删除或注释掉useEffect钩子,因为它不再需要。

您更新后的App.tsx文件现在应如下所示:

import axios from 'axios';import { FC, useState } from 'react';import { AppProps, Users } from './App.types';import User from './components/User';const App: FC<AppProps> = ({ title }) => {  const [users, setUsers] = useState<Users[]>([]);  const [isLoading, setIsLoading] = useState<boolean>(false);  const handleClick = async () => {    try {      setIsLoading(true);      const { data } = await axios.get('https://randomuser.me/api/?results=10');      console.log(data);      setUsers(data.results);    } catch (error) {      console.log(error);    } finally {      setIsLoading(false);    }  };  return (    <div>      <h1>{title}</h1>      <button onClick={handleClick}>显示用户 </button>      {isLoading && <p>加载中...</p>}      <ul>        {users.map(({ login, name, email }) => {          return <User key={login.uuid} name={name} email={email} />;        })}      </ul>    </div>  );};export default App;

现在,如果您检查应用程序,您将只在点击”显示用户”按钮时看到用户被加载。我们还会看到加载消息。

19_load_on_click
在按钮点击时加载用户

如何处理变更事件

现在,让我们添加一个输入字段。当我们在该输入字段中键入任何内容时,我们将在该输入字段下方显示输入的文本。

在按钮后添加一个输入字段,如下所示:

<input type='text' onChange={handleChange} />

并声明一个新的状态来存储输入的值,如下所示:

const [username, setUsername] = useState('');

现在,在App组件中添加handleChange方法,如下所示:

 const handleChange = (event) => {  setUsername(event.target.value);};

然而,您会看到我们得到了一个TypeScript错误,指的是事件参数。

20_event_type
事件类型缺失的TypeScript错误

在TypeScript中,我们总是需要指定每个函数参数的类型。

在这里,TypeScript无法识别event参数的类型。

要查找event参数的类型,我们可以将下面的代码更改为:

<input type='text' onChange={handleChange} />

改为以下代码:

<input type='text' onChange={(event) => {}} />

这里,我们使用了内联函数,因为当使用内联函数时,正确的类型会自动传递给函数参数,所以我们不需要指定它。

如果你将鼠标悬停在event参数上,你将能够看到我们可以在handleChange函数中使用的事件的确切类型,如下所示:

21_change_event_type
使用内联函数确定 TypeScript 事件类型

现在,你可以将下面的代码还原为:

<input type='text' onChange={(event) => {}} />

使用以下代码:

<input type='text' onChange={handleChange} />

现在,让我们在输入框下面显示username状态变量的值:

<input type='text' onChange={handleChange} /><div>{username}</div>

如果你检查应用程序,你将能够看到输入的文本显示在输入框下面。

22_username
在 UI 上显示用户输入的文本

感谢阅读

这就是本教程的全部内容。希望你从中学到了很多。

想观看本教程的视频版本吗?你可以查看这个视频。

你可以在这个仓库中找到这个应用程序的完整源代码。

如果你想精通 JavaScript、ES6+、React 和 Node.js,并有易于理解的内容,请关注我的YouTube 频道。别忘了订阅。

想要及时获取有关 JavaScript、React 和 Node.js 的常规内容吗?在 LinkedIn 上关注我

Learn To Build Expense Manager App Using React And TypeScript


Leave a Reply

Your email address will not be published. Required fields are marked *