React 和 TypeScript 学习指南(一)
原文:
zh.annas-archive.org/md5/5da49be498b161721792aaa3c885dee9译者:飞龙
前言
React 是由 Meta 构建的,旨在为其代码库提供更多结构,并使其能够更好地扩展。React 在 Facebook 上表现得如此出色,以至于他们最终将其开源。如今,React 是构建应用前端的主导技术;它允许我们构建小型、隔离且高度可重用的组件,这些组件可以组合在一起以创建复杂的用户界面。
TypeScript 是由 Microsoft 构建的,旨在帮助开发者更轻松地开发基于 JavaScript 的大型程序。它是 JavaScript 的超集,为它带来了丰富的类型系统。这个类型系统帮助开发者早期捕捉到错误,并允许创建工具来稳健地导航和重构代码。
本书将教会你如何使用这两种技术来创建大型、复杂的用户界面,这些界面易于维护。
这本书面向谁
如果你是一名希望使用 React 和 TypeScript 创建大型和复杂前端开发的开发者,这本书适合你。本书不假设你之前有任何 React 或 TypeScript 的知识——然而,对 JavaScript、HTML 和 CSS 的基本了解将有助于你掌握所涵盖的概念。
本书涵盖的内容
第一章,介绍 React,涵盖了构建 React 组件的基本原理。这包括使用 JSX 定义组件输出,使用 props 使组件可配置,以及使用状态使组件交互。
第二章,介绍 TypeScript,全面讲述了 TypeScript 及其类型系统的基本原理。这包括使用内置类型以及创建新类型。
第三章,设置 React 和 TypeScript,解释了如何创建用于 React 和 TypeScript 开发的工程。然后章节继续介绍如何创建使用 TypeScript 来使 props 和 states 类型安全的 React 组件。
第四章,使用 React Hooks,详细介绍了常见的 React Hooks 及其典型用例。章节还涵盖了如何使用 TypeScript 使 Hooks 类型安全。
第五章,React 前端样式化方法,介绍了如何使用几种不同的方法来样式化 React 组件。每种方法的优点也得到了探讨。
第六章,使用 React Router 进行路由,介绍了一个流行的库,它为多页应用提供了客户端路由。它涵盖了如何声明页面的路径以及如何在这些页面之间创建链接。它还涵盖了如何为高度动态的页面实现页面参数。
第七章,使用表单,探讨了如何使用几种不同的方法来实现表单,包括使用一个流行的库。每种方法的优点也包括在内。
第八章,状态管理,介绍了如何在不同的组件之间共享状态。探讨了多种方法及其优点。
第九章,与 RESTful API 交互,展示了 React 组件如何与 REST API 交互。章节逐步介绍了一种使用核心 React 的方法,然后是使用一个流行库的替代方法。
第十章,与 GraphQL API 交互,展示了 React 组件如何与 GraphQL API 交互。章节详细介绍了如何使用两个不同的流行库来实现这一点。
第十一章,可重用组件,引入了几个使 React 组件高度可重用但仍然类型安全的模式。
第十二章,使用 Jest 和 React Testing Library 进行单元测试,首先深入探讨了如何使用 Jest 测试函数。然后章节转向如何借助 React Testing Library 测试 React 组件。
为了充分利用本书
为了充分利用本书,您需要了解 JavaScript 的基础知识,包括以下内容:
-
理解一些原始的 JavaScript 类型,例如
string、number、boolean、null和undefined -
理解如何创建变量并引用它们,包括数组和对象
-
理解如何创建函数并调用它们
-
理解如何使用
if和else关键字创建条件语句
您还需要了解 HTML 的基础知识,包括以下内容:
-
理解基本的 HTML 元素,如
div、ul、a和h1 -
理解如何引用 CSS 类来设置 HTML 元素的样式
理解基本的 CSS 也有帮助,包括以下内容:
-
如何调整元素大小并包括边距和填充
-
如何定位元素
-
如何为元素着色
为了跟随本书的内容,您需要在您的计算机上安装以下技术:
-
一个现代浏览器,如Google Chrome,可以从
www.google.com/chrome/安装 -
Node.js 和 npm:您可以从
nodejs.org/en/download/安装它们。 -
Visual Studio Code:您可以从:
code.visualstudio.com/安装它。
| 本书涵盖的软件/硬件 |
|---|
| React 18.0 或更高版本 |
| TypeScript 4.7 或更高版本 |
如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将有助于避免与代码复制和粘贴相关的任何潜在错误 。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供包含本书中使用的截图和图表的彩色图像的 PDF 文件。您可以从这里下载:packt.link/5CvU5。
约定使用
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在这里,null被传递,因为没有属性。”
代码块设置如下:
<div className=”title”>
<span>Oh no!</span>
</div>
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
React.createElement(
'span',
null,
title ? title : 'Something important'
);
任何命令行输入或输出都按以下方式编写:
$ mkdir css
$ cd css
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“从管理面板中选择系统信息。”
小贴士或重要提示
看起来是这样的。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告此错误。请访问www.packtpub.com/support/err…并填写表格。
盗版:如果您在互联网上遇到我们作品的任何形式的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
读完*Learn React with TypeScript (Second Edition)*后,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗? 您的电子书购买是否与您选择的设备不兼容?
不要担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您将获得独家折扣、时事通讯和每日免费内容的每日邮箱访问权限
按照以下简单步骤获取优惠:
- 扫描二维码或访问以下链接
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱
第一部分:简介
本部分将帮助您开始学习 React 和 TypeScript,分别了解这两种技术的 fundamentals。然后我们将开始一起使用这些技术,以便我们能够创建强大的类型安全组件。我们还将详细了解 React 的常用 hooks 以及它们在应用程序中的使用情况。
本部分包括以下章节:
-
第一章,介绍 React
-
第二章,介绍 TypeScript
-
第三章,设置 React 和 TypeScript
-
第四章,使用 React Hooks
第一章:介绍 React
Facebook 已经成为一款非常流行的应用。随着其知名度的增长,对新增功能的需求也在增加。React 是 Facebook 为帮助更多人参与代码库并更快地交付功能而提供的解决方案。React 在 Facebook 的工作非常出色,以至于 Meta 最终将其开源。如今,React 是一个成熟的库,用于构建基于组件的前端,它非常受欢迎,拥有庞大的社区和生态系统。
TypeScript 也是一个由另一家大型公司,微软,维护的流行、成熟的库。它允许用户向他们的 JavaScript 代码添加丰富的类型系统,帮助他们提高生产力,尤其是在大型代码库中。
本书将教会你如何使用这两个出色的库来构建易于维护的健壮前端。本书的前两章将分别介绍 React 和 TypeScript。然后,你将学习如何将 React 和 TypeScript 结合起来,使用强类型来构建健壮的组件。本书涵盖了构建网络前端所需的关键主题,例如样式、表单和数据获取。
在本章中,我们将介绍 React 并了解它带来的好处。然后,我们将构建一个简单的 React 组件,学习 JSX 语法和组件属性。之后,我们将学习如何使用组件状态和事件使组件交互。在这个过程中,我们还将学习如何在 JavaScript 模块中组织代码。
在本章结束时,你将能够创建简单的 React 组件,并准备好学习如何使用 TypeScript 强类型化它们。
在本章中,我们将涵盖以下主题:
-
理解 React 的好处
-
理解 JSX
-
创建一个组件
-
理解导入和导出
-
使用属性
-
使用状态
-
使用事件
技术要求
在本章中,我们使用以下工具:
-
浏览器:一个现代浏览器,如 Google Chrome。
-
Babel REPL:我们将使用这个在线工具简要探索 JSX。它可以在
babeljs.io/repl找到。 -
CodeSandbox:我们将使用这个在线工具来构建一个 React 组件。它可以在
codesandbox.io/找到。
本章中所有的代码片段都可以在 github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/tree/main/Chapter1/ 在线找到。
理解 React 的好处
在我们开始创建第一个 React 组件之前,在本节中,我们将了解 React 是什么以及探索其一些好处。
React 非常受欢迎。我们之前提到 Meta 使用 React 为 Facebook 开发,但许多其他知名公司也在使用它,例如 Netflix、Uber 和 Airbnb。React 的流行导致了一个围绕它的大生态系统,其中包括优秀的工具、流行的库和许多经验丰富的开发者。
React 流行的一个原因是它很简单。这是因为它专注于做好一件事 – 提供一个强大的机制来构建 UI 组件。组件是 UI 的组成部分,可以组合在一起来创建前端。此外,组件可以重用,因此可以在不同的屏幕上甚至在其他应用程序中使用。
React 的窄焦点意味着它可以集成到现有的应用程序中,即使它使用不同的框架。这是因为它不需要接管整个应用程序来运行;它乐意作为应用程序前端的一部分运行。
React 组件通过使用 虚拟 DOM(文档对象模型)来高效地显示。你可能熟悉真实的 DOM – 它提供了网页的结构。然而,对真实 DOM 的更改可能会带来高昂的成本,导致交互式应用程序的性能问题。React 通过使用真实 DOM 的内存表示形式,即虚拟 DOM,来解决这个性能问题。在 React 更改真实 DOM 之前,它会生成一个新的虚拟 DOM,并将其与当前的虚拟 DOM 进行比较,以计算对真实 DOM 所需的最小更改量。然后,真实 DOM 使用这些最小更改进行更新。
Meta 使用 React 为 Facebook 开发是一个重大优势,因为它确保了其最高质量 – React 导致 Facebook 出问题对 Meta 来说可不是什么好事!这也意味着在确保新版本的 React 容易采用,从而有助于降低应用程序的维护成本方面投入了大量的思考和关注。
React 的简单性意味着它容易且快速学习。有许多优秀的资源,例如这本书。还有一系列工具,使构建 React 应用程序变得非常容易 – 其中一个工具叫做 Create React App,我们将在 第三章 中学习,设置 React 和 TypeScript。
现在我们开始理解 React 了,让我们在下一节深入探讨,了解 React 组件是如何定义显示内容的。
理解 JSX
JSX 是我们在 React 组件中用来定义组件应显示内容的语法。JSX 代表 JavaScript XML,这开始让我们对它有了些了解。我们将从本节开始学习 JSX,并在在线沙盒中编写一些 JSX 代码。
以下代码片段是一个带有高亮 JSX 的 React 组件:
function App() {
return (
<div className="App">
<Alert type="information" heading="Success">
Everything is really good!
</Alert>
</div>
);
}
你可以看到 JSX 看起来有点像 HTML。然而,它并不是 HTML,因为 HTML 的 div 元素不包含 className 属性,也没有名为 Alert 的元素。JSX 还直接嵌入在 JavaScript 函数中,这有点奇怪,因为 script 元素通常用于在 HTML 中放置 JavaScript。
JSX 是一种 JavaScript 语法扩展。这意味着它不能直接在浏览器中执行 – 首先需要将其转换为 JavaScript。一个可以将 JSX 转换为 JavaScript 的流行工具叫做 Babel。
执行以下步骤来在 Babel 操场中编写你的第一个 JSX:
-
打开浏览器,转到
babeljs.io/repl,并在左侧面板中输入以下 JSX:<span>Oh no!</span>
以下内容出现在右侧面板中,这是我们的 JSX 编译后的结果:
React.createElement("span", null, "Oh no!");
我们可以看到它编译成了一个 React.createElement 函数调用,该调用有三个参数:
-
元素类型可以是 HTML 元素名称(例如
span),React 组件类型或 React 片段类型。 -
包含要应用于元素的属性的对象。在这里,
null被传递,因为没有属性。 -
元素的内容。请注意,在 React 中,元素的内容通常被称为 children。
注意
右侧面板的顶部可能还包含一个 "use strict" 声明,用于指定 JavaScript 将在 严格模式 下运行。严格模式是 JavaScript 引擎在遇到有问题的代码时抛出错误,而不是忽略它。有关 JavaScript 中严格模式的更多信息,请参阅以下链接:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode。
你也可能在右侧面板中看到 /*#__PURE__*/ 注释。这些注释有助于打包器(如 webpack)在打包过程中删除冗余代码。我们将在 第三章 设置 React 和 TypeScript 中学习关于 webpack 的内容。
-
让我们通过将
div元素放在span元素周围来扩展我们的示例,如下代码片段所示:<div className="title"><span>Oh no!</span></div>
这现在编译成了两个对 React.createElement 的函数调用,其中 span 被传递为 div 的子元素:
React.createElement("div", {
className: "title"
}, React.createElement("span", null, "Oh no!"));
我们还可以看到一个 className 属性,通过 div 元素传递了 "title" 值。
注意
我们已经看到,React 使用 className 属性而不是 class 来引用 CSS 类。这是因为 class 是 JavaScript 中的一个关键字,使用它会导致错误。
-
现在我们来做一些真正有趣的事情。让我们在 JSX 中嵌入一些 JavaScript。所以,进行以下高亮更改:
const title = "Oh no!";<div className="title"><span>{title}</span></div>
我们声明了一个 title JavaScript 变量,将其赋值为 "Oh no!",并将其嵌入到 span 元素中。
注意,title 变量被放置在元素内部的括号中。任何 JavaScript 代码都可以通过括号包围的方式嵌入到 JSX 中。
我们现在的代码编译成了以下内容:
const title = "Oh no!";
React.createElement("div", {
className: "title"
}, React.createElement("span", null, title));
-
为了进一步说明 JavaScript 在 JSX 中的使用,让我们在
span元素内部使用一个 JavaScript 三元表达式。添加以下三元表达式:const title = "Oh no!";<div className="title"><span>{title ? title : "Something important"}</span></div>
三元表达式是 JavaScript 中的一个内联条件语句。表达式从条件开始,后跟?,然后是条件为真时返回的内容,接着是:,最后是条件为假时返回的内容。有关三元表达式的更多信息,请参阅以下链接:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator。
我们看到嵌套调用React.createElement使用三元表达式作为span的子元素:
React.createElement(
"span",
null,
title ? title : "Something important"
);
这完成了我们在 Babel playground 中对 JSX 的探索。
总结来说,JSX 可以被视为 HTML 和 JavaScript 的混合,用于指定 React 组件的输出。JSX 需要使用像 Babel 这样的工具将其转换为 JavaScript。有关 JSX 的更多信息,请参阅以下链接:reactjs.org/docs/introducing-jsx.html。
现在我们对 JSX 有了更深入的了解,我们将在下一节创建我们的第一个 React 组件。
创建组件
在本节中,我们将使用一个名为 CodeSandbox 的在线工具来创建一个 React 项目。在创建一个基本的 React 组件之前,我们将花时间了解 React 应用的入口点和组件在项目中的结构。
创建 CodeSandbox 项目
CodeSandbox 的伟大之处在于我们可以在网页浏览器中点击一下按钮就创建一个 React 项目,然后专注于如何创建 React 组件。请注意,我们将在第三章“设置 React 和 TypeScript”中学习如何在本地计算机上的代码编辑器中创建 React 项目。我们本章的重点是学习 React 基础知识。
现在,让我们执行以下步骤在 CodeSandbox 中创建一个 React 组件:
- 在浏览器中转到
codesandbox.io/并点击页面右侧的Create Sandbox按钮。
注意
如果你想,可以创建一个 CodeSandbox 账户,但你也可以作为一个匿名用户创建一个 React 项目。
- 出现了一个项目模板列表。点击React模板(不要选择React TypeScript模板,因为我们本章将专注于 React)。
几秒钟后,将创建一个 React 项目:
图 1.1 – CodeSandbox 中的 React 项目
CodeSandbox 编辑器中有三个主要面板:
-
文件面板:这通常在左侧,包含项目中的所有文件。
-
代码编辑器面板:这通常是中间面板,包含代码。这是我们编写 React 组件代码的地方。在文件面板中点击一个文件,它将在代码编辑器面板中打开。
-
浏览器面板:这显示正在运行的应用的预览,通常在右侧。
现在我们已经创建了一个 React 项目,我们将花一些时间来了解应用的入口点。
理解 React 入口点
这个 React 应用的入口点在 index.js 文件中。通过在 文件 面板中单击它来打开此文件并检查其内容:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
这段代码中有很多内容。以下是每行代码的解释(如果你在本书的这一部分还没有完全理解,不要担心,你很快就会明白):
-
第一条语句从 React 中导入了一个
StrictMode组件。这意味着在文件中的代码将使用来自react库的StrictMode组件。我们将在下一节详细讲解导入语句。 -
第二条语句从 React 中导入了一个
createRoot函数。 -
第三条导入语句从我们项目的
App.js文件中导入了一个App组件。 -
然后将一个
rootElement变量赋值给一个具有id为"root"的 DOM 元素。 -
React 的
createRoot函数接收一个 DOM 元素并返回一个变量,该变量可以用来显示 React 组件树。然后将rootElement变量传递给createRoot,并将结果赋值给root变量。 -
在
root变量上调用render函数,传递包含嵌套App组件的StrictMode组件的 JSX。render函数在页面上显示 React 组件。这个过程通常被称为 渲染。
注意
StrictMode 组件将检查其内部的内容以查找潜在的问题,并在浏览器控制台中报告它们。这通常被称为 React 的严格模式。React 中的严格模式与 JavaScript 中的严格模式不同,但它们消除坏代码的目的相同。
总结来说,index.js 中的代码在具有 id 为 "root" 的 DOM 元素中以 React 的严格模式渲染了 App 组件。
接下来,我们将花一些时间来了解 React 组件树以及 index.js 中引用的 App 组件。
理解 React 组件树
一个 React 应用由组件和 DOM 元素的树状结构组成。树的根组件是树顶部的组件。在我们的 CodeSandbox 项目中,根组件是 StrictMode 组件。
React 组件可以嵌套在另一个 React 组件内部。在 CodeSandbox 项目中,App 组件嵌套在 StrictMode 组件内部。这是一种强大的组件组合方式,因为任何组件都可以放在 StrictMode 内部——它不一定是 App。
React 组件可以在它们的 JSX 中引用一个或多个其他组件,甚至 DOM 元素。打开 App.js 文件并观察它引用了 DOM 元素 div、h1 和 h2:
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
CodeSandbox 项目的组件树构建如下:
StrictMode
└── App
└── div
└── h1
└── h2
总结来说,一个 React 应用由 React 组件和 DOM 元素的树状结构组成。
接下来,是时候创建一个 React 组件了。
创建一个基本的 alert 组件
现在,我们将创建一个显示警告的组件,我们将其简单地称为Alert。它将包括一个图标、一个标题和一条消息。
重要提示
React 组件名称必须以大写字母开头。如果组件名称以小写字母开头,它将被视为 DOM 元素,并且无法正确渲染。有关更多信息,请参阅 React 文档中的以下链接:reactjs.org/docs/jsx-in-depth.html#user-defined-components-must-be-capitalized。
执行以下步骤以在 CodeSandbox 项目中创建组件:
-
在
src文件夹中,并在出现的菜单中选择创建文件。 -
光标放置在一个新文件中,准备您输入组件文件名。将文件名输入为
Alert.js并按Enter键。
注意
组件文件的文件名对 React 或 React 转译器来说并不重要。通常的做法是将文件名与组件同名,无论是 Pascal 大小写还是 snake 大小写。然而,文件扩展名必须是.js或.jsx,以便 React 转译器能够识别这些为 React 组件。
-
Alert.js文件将在代码编辑器面板中自动打开。将以下代码输入到该文件中:function Alert() {return (<div><div><span role="img" aria-label="Warning">⚠</span><span>Oh no!</span></div><div>Something isn't quite right ...</div></div>);}
请记住,代码片段可在网上找到以供复制。上一个代码片段的链接可在github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/blob/main/Chapter1/Section3-Creating-a-component/Alert.js找到。
该组件渲染以下项目:
-
一个警告图标(请注意,这是一个警告表情符号)。
-
一个标题,哦不!。
-
一条消息,有些地方不太对…。
注意
role和aria-label属性已添加到包含警告图标的span元素中,以帮助屏幕阅读器理解这是一个具有警告标题的图像。
有关img角色的更多信息,请参阅developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/img_role。
有关aria-label属性的更多信息,请参阅developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label。
或者,可以使用箭头函数语法实现 React 组件。以下代码片段是Alert组件的箭头函数语法版本:
const Alert = () => {
return (
<div>
<div>
<span role="img" aria-label="Warning">
⚠
</span>
<span>Oh no!</span>
</div>
<div>Something isn't quite right ...</div>
</div>
);
};
注意
在 React 函数组件的上下文中,箭头函数和普通函数之间没有显著的区别。所以,选择哪一个取决于个人喜好。本书通常使用常规函数语法,因为它需要输入更少的字符,然而,如果你愿意,你可以在以下链接中找到有关 JavaScript 箭头函数的更多信息:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions。
恭喜你,你已经创建了你的第一个 React 组件。让我们快速回顾本节的关键点:
-
在 React 应用中的入口点通常是
index.js。 -
React 的
createRoot函数允许 React 组件树在 DOM 元素内渲染。 -
一个 React 组件是一个以大写字母开头的 JavaScript 函数。该函数使用 JSX 语法返回应显示的内容。
你可能已经注意到 alert 组件没有出现在 import 和 export 语句中。
理解导入和导出
import 和 export 语句允许 JavaScript 被结构化为模块。
本节将首先介绍为什么 JavaScript 模块很重要,然后如何使用 import 和 export 语句定义和使用它们。然后我们将利用这些知识将 alert 组件添加到 CodeSandbox 项目的 React 组件树中。
理解模块的重要性
默认情况下,JavaScript 代码在所谓的全局作用域中执行。这意味着一个文件中的代码会自动在另一个文件中可用。不幸的是,这意味着我们实现的函数可能会覆盖其他文件中具有相同名称的函数。你可以想象这种结构很快就会变得具有挑战性和风险,难以维护。
幸运的是,JavaScript 有一个模块功能。模块的函数和变量是隔离的,因此不同模块中具有相同名称的函数不会冲突。这是一种更安全的代码结构方式,并且在构建 React 应用时是一种常见的做法。
接下来,我们将学习如何定义模块。
使用导出语句
模块是一个至少包含一个 export 语句的文件。export 语句引用了可供其他模块使用的成员。可以将此视为使成员公开可用。成员可以是文件中的函数、类或变量。未包含在 export 语句中的成员是私有的,且在模块外部不可用。
以下代码语句是一个带有其 export 语句高亮的模块示例。这被称为命名导出语句,因为公开成员被明确命名:
function myFunc1() {
...
}
function myFunc2() {
...
}
function myFunc3() {
...
}
export { myFunc1, myFunc3 };
在示例中,myFunc1 和 myFunc3 函数是公开的,而 myFunc2 是私有的。
或者,可以在公共函数之前添加 export 关键字:
export function myFunc1() {
...
}
function myFunc2() {
...
}
export function myFunc3() {
...
}
本书将使用 export 关键字方法,因为这样可以立即清楚地知道哪个函数是公开的。在文件底部有一个单独的 export 语句,你必须继续滚动到文件底部才能找出一个函数是否是公开的。
export 语句定义在模块的底部:
export default myFunc1;
default 关键字表示导出是一个默认 export 语句。
第二种变体是在成员前添加了 export 和 default 关键字:
export default function myFunc1() {
...
}
本书通常使用命名导出而不是默认导出。
接下来,我们将学习关于 import 语句的内容。
使用 import 语句
使用 import 语句允许使用模块的公共成员。与 export 语句一样,有 import 语句。默认 import 语句只能用于引用默认 export 语句。
这里是一个默认 import 语句的例子:
import myFunc1 from './myModule';
从 myModule.js 文件中导入了默认导出成员,并命名为 myFunc1。
注意
导入的默认成员的名称不一定需要与默认导出成员的名称匹配,但这样做是一种常见的做法。
这里是一个命名 import 语句的例子:
import { myFunc1, myFunc3 } from './myModule';
在这里,从 myModule.js 文件中导入了名为 myFunc1 和 myFunc3 的命名导出成员。
注意
与默认导入不同,导入成员的名称必须与导出成员的名称匹配。
现在我们已经了解了如何将 JavaScript 代码结构化为模块,我们将使用这些知识将 alert 组件添加到 CodeSandbox 项目的 React 组件树中。
将 Alert 添加到 App 组件中
回到我们的 CodeSandbox 项目中的 Alert 组件,我们将在 App 组件中引用 Alert。为此,执行以下步骤:
-
首先,我们需要导出
Alert组件。打开Alert.js并在Alert函数之前添加export关键字:export function Alert() {...}
注意
将每个 React 组件放在单独的文件中,并因此有一个单独的模块,这是一种常见的做法。这可以防止文件变得过大,并有助于代码库的可读性。
-
现在我们可以将
Alert导入到App.js文件中。打开App.js并在文件顶部添加高亮的import语句:import { Alert } from './Alert';import "./styles.css";export default function App() {...} -
我们现在可以在
App组件的 JSX 中引用Alert。在div元素内部添加高亮行,替换其现有内容:export default function App() {return (<div className="App"><Alert /></div>);}
该组件将在 浏览器 面板中显示以下内容:
图 1.2 – 浏览器面板中的 alert 组件
很好!如果你注意到 alert 组件的样式并不美观,不要担心——我们将在 第四章 React 前端样式方法 中学习如何对其进行样式化。
这里是对本节中几个关键点的回顾:
-
React 应用程序使用 JavaScript 模块结构化,以帮助代码库可维护。
-
通常,一个 React 组件在其自己的模块中结构化,因此在使用之前需要导出和导入到另一个 React 组件中。
接下来,我们将学习如何使警告组件更加灵活。
使用属性
目前,警告组件相当不灵活。例如,警告消费者不能更改标题或消息。目前,标题或消息需要在 Alert 本身内进行更改。属性 解决了这个问题,我们将在本节中学习它们。
注意
Props 是 属性 的缩写。React 社区通常将它们称为 props,因此我们将在本书中这样做。
理解属性
props 是一个可选参数,它被传递到一个 React 组件中。该参数是一个包含我们选择的属性的对象。以下代码片段显示了 ContactDetails 组件中的 props 参数:
function ContactDetails(props) {
console.log(props.name);
console.log(props.email);
...
}
在前面的代码片段中,props 参数包含 name 和 email 属性。
注意
参数不必命名为 props,但这是常见的做法。
属性作为属性在 JSX 中传递给组件。属性名称必须与组件中定义的名称匹配。以下是一个将属性传递给前面的 ContactDetails 组件的示例:
<ContactDetails name="Fred" email="fred@somewhere.com" />
因此,属性使组件输出更加灵活。组件的消费者可以将适当的属性传递到组件中,以获得所需输出。
接下来,我们将向我们所工作的警告组件添加一些属性。
向警告组件添加属性
在 CodeSandbox 项目中,按照以下步骤向警告组件添加属性以使其更加灵活:
-
打开
alert.js并向函数添加一个props参数:export function Alert(props) {...} -
我们将为警告定义以下属性:
-
type: 这将是"information"或"warning",并确定警告中的图标。 -
heading: 这将确定警告的标题。 -
children: 这将确定警告的内容。children属性实际上是一个用于组件主要内容的特殊属性。
-
更新警告组件的 JSX 以使用属性如下:
export function Alert(props) {
return (
<div>
<div>
<span
role="img"
aria-label={
props.type === "warning"
? "Warning"
: "Information"
}
>
{props.type === "warning" ? "⚠" : "ℹ"}
</span>
<span>{props.heading}</span>
</div>
<div>{props.children}</div>
</div>
);
}
注意到 App 组件还没有向 Alert 传递任何属性:
图 1.3 – 只显示信息图标的警告组件
-
打开
App.js并更新 JSX 中的Alert组件,如下传递属性:export default function App() {return (<div className="App"><Alert type="information" heading="Success">Everything is really good!</Alert></div>);}
注意到 Alert 组件不再自动关闭,因此可以将 Everything is really good! 传递到其内容中。内容是通过 children 属性传递的。
浏览器 面板现在显示配置的警告组件:
图 1.4 – 浏览器面板中配置的警告组件
- 我们可以通过解构
props参数来稍微清理一下警告组件的代码。
注意
解构是 JavaScript 的一个特性,允许从对象中提取属性。更多信息,请参阅以下链接:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment。
再次打开Alert.js,解构function参数,并如下使用解包的 props:
export function Alert({ type, heading, children }) {
return (
<div>
<div>
<span
role="img"
aria-label={
type === "warning" ? "Warning" : "Information"
}
>
{type === "warning" ? "⚠" : "ℹ"}
</span>
<span>{heading}</span>
</div>
<div>{children}</div>
</div>
);
}
这更简洁,因为我们直接使用解包的 props,而不是通过props参数引用它们。
-
我们希望
type属性默认为"information"。如下定义此默认值:export function Alert({type = "information",heading,children}) {...}
至此,警报组件的 props 实现已完成。以下是对 props 的快速回顾:
-
Props 允许通过消费 JSX 来配置组件,并作为 JSX 属性传递。
-
Props 作为对象参数在组件定义中接收,然后可以在其 JSX 中使用。
接下来,我们将继续改进警报组件,允许用户关闭它。
使用状态
组件状态是一个特殊的变量,包含有关组件当前情况的信息。例如,组件可能处于加载状态或错误状态。
在本节中,我们将学习状态,并在 CodeSandbox 项目中使用它来构建我们的警报组件。我们将使用状态来允许用户关闭警报。
理解状态
没有预定义的状态列表;我们为特定组件定义适当的状态。有些组件甚至不需要任何状态;例如,在我们 CodeSandbox 项目中的App和Alert组件到目前为止还没有需要状态的要求。
然而,状态是使组件交互的关键部分。当用户与组件交互时,组件的输出可能需要改变。例如,点击一个组件可能需要使组件中的某个元素不可见。组件状态的改变会导致组件刷新,这通常被称为重新渲染。因此,用户点击组件可能导致状态改变,从而使组件中的某个元素变得不可见。
状态是通过 React 的useState函数定义的。useState函数是 React 的钩子之一。React 钩子是在 React 的 16.8 版本中引入的,为函数组件提供了强大的功能,如状态。关于 React 钩子有一个专门的章节,即第四章,使用 React Hooks。
useState的语法如下:
const [state, setState] = useState(initialState);
这里是关键点:
-
初始状态值传递给
useState。如果没有传递值,它将初始化为undefined。 -
useState返回一个包含当前状态值和更新状态值函数的元组。在先前的代码片段中,该元组被解构。 -
在先前的代码片段中,状态变量名为
state,但我们可以选择任何有意义的名称。 -
我们还可以选择状态设置函数的名称,但通常的做法是使用与状态变量相同的名称,并在其前面加上
set。 -
可以通过定义多个
useState实例来定义多个状态。例如,以下是加载和错误状态的定义:const [loading, setLoading] = useState(true);const [error, setError] = useState();
接下来,我们将在警报组件中实现状态以确定其是否可见。
在警报组件中实现可见状态
我们将首先在警报组件中实现一个功能,允许用户关闭它。该功能的关键部分是控制警报的可见性,我们将使用visible状态来实现。此状态将是true或false,并且最初将其设置为true。
按照以下步骤在Alert中实现visible状态:
-
在 CodeSandbox 项目中打开
Alert.js。 -
在文件顶部添加以下
import语句以从 React 导入useState钩子:import { useState } from 'react'; -
在组件定义中如下定义
visible状态:export function Alert(...) {const [visible, setVisible] = useState(true);return (...);} -
在状态声明之后,添加一个条件,如果
visible状态为false,则返回null。这意味着将不会渲染任何内容:export function Alert(...) {const [visible, setVisible] = useState(true);if (!visible) {return null;}return (...);}
当visible状态为true时,组件将进行渲染。尝试将初始状态值更改为false,你将在浏览器面板中看到它消失。
目前,警报组件正在使用visible状态的值,如果它是false,则不渲染任何内容。然而,组件尚未更新visible状态——也就是说,setVisible目前未使用。我们将在实现关闭按钮后更新visible状态,我们将在下一部分实现。
向警报中添加关闭按钮
我们将在警报组件中添加一个关闭按钮,允许用户关闭它。我们将使其可配置,以便警报消费者可以选择是否渲染关闭按钮。
执行以下步骤:
-
首先打开
Alert.js并添加一个closable属性:export function Alert({type = "information",heading,children,closable}) {...}
警报组件的消费者将使用closable属性来指定是否显示关闭按钮。
-
按照以下方式在标题和内容之间添加一个关闭按钮:
export function Alert(...) {...return (<div><div>...<span>{heading}</span></div><button aria-label="Close"><span role="img" aria-label="Close">❌</span></button><div>{children}</div></div>);}
注意,包含关闭图标的span元素被赋予了"img"角色和"Close"标签,以帮助屏幕阅读器。同样,按钮也被赋予了"Close"标签以帮助屏幕阅读器。
关闭按钮在警报组件中的显示方式如下:
图 1.5 – 警报组件中的关闭按钮
-
目前,关闭按钮总是渲染,而不仅仅是当
closable属性为true时。我们可以使用 JavaScript 逻辑短路表达式(由&&字符表示)有条件地渲染close按钮。为此,进行以下突出显示的更改:import { useState } from 'react';export function Alert(...) {...return (<div><div>...<span>{heading}</span></div>{closable && (<button aria-label="Close"><span role="img" aria-label="Close">❌</span></button>)}<div>{children}</div></div>);}
如果closable是真值,则按钮将被渲染。
注意
有关逻辑AND短路表达式的更多信息,请参阅以下链接:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND。
有关 JavaScript 的假值,请参阅以下链接:developer.mozilla.org/en-US/docs/Glossary/Falsy,有关真值,请参阅以下链接:developer.mozilla.org/en-US/docs/Glossary/Truthy。
-
打开
App.js并将closable属性传递给Alert:export default function App() {return (<div className="App"><Alert type="information" heading="Success" closable>Everything is really good!</Alert></div>);}
注意,closable属性上没有明确定义值。我们可以按照以下方式传递值:
closable={true}
然而,没有必要在布尔属性上传递值。如果一个元素上存在布尔属性,其值将自动为true。
当指定了closable属性时,关闭按钮在警告组件中显示,就像在图 1**.5中之前那样。但是当没有指定closable属性时,关闭按钮不会显示:
图 1.6 – 当未指定 closable 时,关闭按钮不在警告组件中
优秀!
对我们迄今为止关于 React 状态的了解进行快速回顾:
-
使用 React 的
useState钩子定义状态 -
状态的初始值可以通过
useState钩子传递 -
useState返回一个状态变量,可以用来有条件地渲染元素 -
useState还返回一个函数,可以用来更新状态的值
你可能已经注意到,关闭按钮实际上并没有关闭警告。在下一节中,我们将通过学习 React 中的事件来纠正这一点。
使用事件
事件是允许组件具有交互性的另一个关键部分。在本节中,我们将了解 React 事件是什么以及如何在 DOM 元素上使用事件。我们还将学习如何创建我们自己的 React 事件。
随着我们学习事件,我们将继续扩展警告组件的功能。我们将先完成关闭按钮的实现,然后再创建一个当警告被关闭时的事件。
理解事件
浏览器事件发生在用户与 DOM 元素交互时。例如,点击按钮会从该按钮引发一个click事件。
当事件被引发时,可以执行逻辑。例如,当关闭按钮被点击时,警告可以关闭。可以注册一个名为事件处理器(有时称为事件监听器)的函数,用于包含在元素事件中,当该事件发生时执行逻辑。
注意
有关浏览器事件的更多信息,请参阅以下链接:developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events。
React 中的事件与浏览器原生事件非常相似。事实上,React 事件是浏览器原生事件的一个包装器。
React 中的事件处理程序通常使用属性在 JSX 中注册到元素上。以下代码片段在button元素上注册了一个名为handleClick的click事件处理程序:
<button onClick={handleClick}>...</button>
接下来,我们将回到我们的警告组件,并在关闭按钮上实现一个click处理程序来关闭警告。
在警告中实现关闭按钮点击处理程序
目前,我们的警告组件包含一个关闭按钮,但点击时没有任何反应。警告还包含一个visible状态,它决定了警告是否显示。因此,为了完成关闭按钮的实现,我们需要在点击时添加一个事件处理程序来将visible状态设置为false。执行以下步骤来完成此操作:
-
打开
Alert.js并在关闭按钮上注册一个如下所示的click处理程序:<button aria-label="Close" onClick={handleCloseClick}>
我们已经在关闭按钮上注册了一个名为handleCloseClick的click处理程序。
-
然后,我们需要在组件中实现
handleCloseClick函数。从创建一个位于return语句之上的空函数开始:export function Alert(...) {const [visible, setVisible] = useState(true);if (!visible) {return null;}function handleCloseClick() {}return (...);}
这可能看起来有点奇怪,因为我们已经将handleCloseClick函数放在了另一个函数Alert内部。处理程序需要放在Alert函数内部;否则,警告组件将无法访问它。
如果喜欢,可以使用箭头函数语法来编写事件处理程序。处理程序的箭头函数版本如下:
export function Alert(...) {
const [visible, setVisible] = useState(true);
if (!visible) {
return null;
}
const handleCloseClick = () => {}
return (
...
);
}
事件处理程序也可以直接在 JSX 元素上添加,如下所示:
<button aria-label="Close" onClick={() => {}}>
在警告组件中,我们将坚持使用命名的handleCloseClick事件处理程序函数。
-
现在我们可以使用
visible状态设置函数在事件处理程序中将visible状态设置为false:function handleCloseClick() {setVisible(false);}
如果你点击浏览器面板中的关闭按钮,警告就会消失。太棒了!
刷新图标可以被点击,使组件在浏览器面板中重新出现:
图 1.7 – 浏览器面板刷新选项
接下来,我们将扩展关闭按钮,以便在警告关闭时触发一个事件。
实现警告关闭事件
现在,我们将在警告组件中创建一个自定义事件。当警告关闭时,将触发此事件,以便消费者可以在发生这种情况时执行逻辑。
组件中的自定义事件是通过实现一个属性来实现的。这个属性是一个在触发事件时被调用的函数。
要实现一个关闭警告事件,请按照以下步骤操作:
-
首先打开
Alert.js并为事件添加一个属性:export function Alert({type = "information",heading,children,closable,onClose}) {}
我们将属性命名为onClose。
注意
通常,事件属性的名称以on开头。
-
在
handleCloseClick事件处理程序中,在将visible状态设置为false之后触发关闭事件:function handleCloseClick() {setVisible(false);if (onClose) {onClose();}}
注意,我们只有在onClose被定义并且由消费者作为属性传递时才调用它。这意味着我们并没有强迫消费者处理这个事件。
-
我们现在可以在
App组件中处理 alert 被关闭的情况。打开App.js并在 JSX 中的Alert上添加以下事件处理器:<Alerttype="information"heading="Success"closableonClose={() => console.log("closed")}>Everything is really good!</Alert>;
我们这次使用了内联事件处理器。
在 浏览器 面板中,如果你点击关闭按钮并查看控制台,你会看到输出了 closed:
图 1.8 – 浏览器面板关闭控制台输出
这就完成了关闭事件和本章 alert 的实现。
这是关于 React 事件我们所学到的:
-
事件,连同状态,使组件具有交互性
-
事件处理器是在 JSX 元素上注册的函数
-
通过实现一个函数属性并调用它来引发事件,可以创建一个自定义事件
本章中我们创建的组件是一个函数组件。你还可以使用类来创建组件。例如,alert 组件的类组件版本在 github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/blob/main/Chapter1/Class-component/Alert.js。然而,由于以下原因,函数组件在 React 社区中占主导地位:
-
通常,它们需要更少的代码来实现
-
组件内的逻辑可以更容易地重用
-
实现方式非常不同
由于这些原因,我们将专注于本书中的函数组件。
接下来,我们将总结本章所学的内容。
摘要
我们现在明白 React 是一个流行的库,用于创建基于组件的前端。在本章中,我们使用 React 创建了一个 alert 组件。
组件输出使用 HTML 和 JavaScript 的混合体 JSX 声明。JSX 需要被转换为 JavaScript 才能在浏览器中执行。
可以将属性作为 JSX 属性传递给组件。这允许组件的消费者控制其输出和行为。组件接收属性作为一个对象参数。JSX 属性名称形成对象参数属性名称。我们在本章的 alert 组件中实现了一系列属性。
事件可以被处理,以便在用户与组件交互时执行逻辑。我们在 alert 组件中创建了一个关闭按钮点击事件的处理器。
可以使用 useState 钩子来定义状态,以重新渲染组件并更新其输出。状态通常在事件处理器中更新。我们为 alert 是否可见创建了状态。
可以通过实现一个函数属性来创建自定义事件。这允许组件的消费者在用户与其交互时执行逻辑。我们在 alert 组件上实现了一个关闭事件。
在下一章,我们将介绍 TypeScript。
问题
回答以下问题以巩固你在本章中学到的内容:
-
以下组件定义有什么问题?
export function important() {return <div>This is really important!</div>;} -
带有属性的组件如下定义:
export function Name({ name }) {return <div>name</div>;}
尽管属性值没有输出。问题是什么?
-
组件属性如下传递给组件:
<ContactDetails name="Fred" email="fred@somewhere.com" />
然后将组件定义为以下内容:
export function ContactDetails({ firstName, email }) {
return (
<div>
<div>{firstName}</div>
<div>{email}</div>
</div>
);
}
尽管没有输出名字Fred。问题是什么?
-
以下 JSX 中处理
click事件的方式有什么问题?<button click={() => console.log("clicked")}>Click me</button>; -
这里定义的
loading状态的初始值是什么?const [loading, setLoading] = useState(true); -
以下组件中设置状态的方式有什么问题?
export function Agree() {const [agree, setAgree] = useState();return (<button onClick={() => agree = true}>Click to agree</button>);} -
以下组件实现了一个可选的
Agree事件。这个实现有什么问题?export function Agree({ onAgree }) {function handleClick() {onAgree();}return (<button onClick={handleClick}>Click to agree</button>);}
答案
这里是关于你在本章中学到的内容的问答:
-
组件定义的问题在于其名称是小写的。React 函数必须以大写字母开头命名:
export function Important() {...} -
问题在于
div元素内部的name变量没有被大括号括起来。所以,将输出单词name而不是name属性的值。以下是组件的修正版本:export function Name({ name }) {return <div>{name}</div>;} -
问题在于传递了一个
name属性而不是firstName。以下是修正后的 JSX:<ContactDetails firstName="Fred" email="fred@somewhere.com" /> -
问题在于传递了一个
click属性而不是onClick。以下是修正后的 JSX:<button onClick={() => console.log("clicked")}>Click me</button>; -
loading状态初始值是true。 -
状态没有使用状态设置函数进行更新。以下是设置状态的修正版本:
export function Agree() {const [agree, setAgree] = useState();return (<button onClick={() => setAgree(true)}>Click to agree</button>);} -
问题在于如果
onAgree没有被传递,点击按钮将导致错误,因为它将是undefined。以下是组件的修正版本:export function Agree({ onAgree }) {function handleClick() {if (onAgree) {onAgree();}}return (<button onClick={handleClick}>Click to agree</button>);}
第二章:介绍 TypeScript
在本章中,我们将首先了解 TypeScript 是什么,以及它是如何在 JavaScript 之上提供更丰富的类型系统的。我们将学习 TypeScript 中的基本类型,如数字和字符串,然后学习如何使用不同的 TypeScript 功能创建自己的类型来表示对象和数组。最后,我们将通过理解 TypeScript 编译器及其在 React 应用程序中的关键选项来结束本章。
到本章结束时,你将准备好学习如何使用 TypeScript 来构建带有 React 的前端。
在本章中,我们将涵盖以下主题:
-
理解 TypeScript 的好处
-
理解 JavaScript 类型
-
使用基本的 TypeScript 类型
-
创建 TypeScript 类型
-
使用 TypeScript 编译器
技术要求
在本章中,我们将使用以下技术:
-
浏览器:一个现代浏览器,如 Google Chrome。
-
TypeScript Playground:这是一个位于 www.typescriptlang.org/play/ 的网站,允许您在不安装 TypeScript 的情况下探索其功能和理解其特性。
-
CodeSandbox:我们将简要使用这个在线工具来探索 JavaScript 的类型系统。这个工具可以在
codesandbox.io/找到。 -
Visual Studio Code:我们需要一个编辑器来体验 TypeScript 的好处并探索 TypeScript 编译器。这个编辑器可以从
code.visualstudio.com/安装。其他可用的编辑器可以在github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support找到。 -
Node.js 和 npm:TypeScript 依赖于这些软件组件。您可以从
nodejs.org/en/download/安装它们。
本章中的所有代码片段都可以在以下网址找到:github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/tree/main/Chapter2。
理解 TypeScript 的好处
在本节中,我们将首先了解 TypeScript 是什么,它与 JavaScript 的关系,以及 TypeScript 如何使团队更高效。
理解 TypeScript
TypeScript 首次于 2012 年发布,并且仍在开发中,每隔几个月就会发布新版本。但 TypeScript 是什么,它有哪些好处?
TypeScript 通常被称为 JavaScript 的超集或扩展,因为 JavaScript 中的任何功能在 TypeScript 中都是可用的。与 JavaScript 不同,TypeScript 不能直接在浏览器中执行 - 它必须首先转换为 JavaScript。
注意
值得注意的是,有一个提案正在考虑中,该提案将允许 TypeScript 在不进行转换的情况下直接在浏览器中执行。有关更多信息,请参阅以下链接:github.com/tc39/proposal-type-annotations。
TypeScript 为 JavaScript 添加了一个丰富的类型系统。它通常与 Angular、Vue 和 React 等前端框架一起使用。TypeScript 还可用于使用 Node.js 构建后端。这展示了 TypeScript 类型系统的灵活性。
当 JavaScript 代码库增长时,它可能变得难以阅读和维护。TypeScript 的类型系统解决了这个问题。TypeScript 使用类型系统允许代码编辑器在开发者编写有问题的代码时捕获类型错误。代码编辑器还使用类型系统提供生产力功能,如强大的代码导航和代码重构。
接下来,我们将通过一个示例来了解 TypeScript 如何捕获 JavaScript 无法捕获的错误。
提前捕获类型错误
类型信息帮助 TypeScript 编译器捕获类型错误。在 Visual Studio Code 等代码编辑器中,类型错误在开发者犯下类型错误后立即用红色下划线标出。执行以下步骤以体验 TypeScript 捕获类型错误的示例:
-
在您选择的文件夹中打开 Visual Studio Code。
-
通过在 EXPLORER 面板中选择 新建文件 选项创建一个名为
calculateTotalPrice.js的新文件。
图 2.1 – 在 Visual Studio Code 中创建新文件
-
将以下代码输入到文件中:
function calculateTotalPriceJS(product, quantity, discount) {const priceWithoutDiscount = product.price * quantity;const discountAmount = priceWithoutDiscount * discount;return priceWithoutDiscount - discountAmount;}
记住,代码片段可在网上找到以供复制。上一个代码片段的链接可在 github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/blob/main/Chapter2/Section1-Understanding-TypeScript/calculateTotalPrice.js 找到。
代码中可能存在一个难以发现的错误,并且 Visual Studio Code 不会突出显示该错误。
- 现在创建一个文件的副本,但使用
.ts扩展名而不是.js。可以通过在 EXPLORER 面板中右键单击文件并选择 复制 选项来复制文件。然后再次右键单击 EXPLORER 面板并选择 粘贴 选项以创建复制的文件。
注意
.ts 文件扩展名表示 TypeScript 文件。这意味着 TypeScript 编译器将对这个文件执行类型检查。
-
在
calculateTotalPrice.ts文件中,从函数名称的末尾移除JS并对代码进行以下突出显示的更新:function calculateTotalPrice(product: { name: string; unitPrice: number },quantity: number,discount: number) {const priceWithoutDiscount = product.price * quantity;const discountAmount = priceWithoutDiscount * discount;return priceWithoutDiscount - discountAmount;}
在这里,我们添加了 TypeScript function 参数。我们将在下一节中详细介绍类型注解。
关键点是类型错误现在用红色波浪线突出显示:
图 2.2 – 高亮显示的类型错误
错误在于函数引用了产品对象中不存在的price属性。应该引用的属性是unitPrice。
在开发过程中早期捕捉这些问题可以提高团队的生产率,并且是质量保证需要捕捉的更少的一件事。情况可能会更糟——错误可能会进入实时应用程序,给用户带来不良体验。
请保持这些文件在 Visual Studio Code 中打开,因为我们将在下一个示例中运行 TypeScript 如何提高开发体验的示例。
使用 IntelliSense 提高开发体验和生产力
IntelliSense是代码编辑器中的一个功能,它提供了关于代码元素的有用信息,并允许快速完成代码。例如,IntelliSense 可以提供对象中可用的属性列表。
执行以下步骤以体验 TypeScript 与 IntelliSense 相比 JavaScript 如何工作得更好,以及这对生产力的积极影响。作为这项练习的一部分,我们将修复上一节中的价格错误:
- 打开
calculateTotalPrice.js文件,在第 2 行,即product.price被引用的地方,移除price。然后,将光标放在点(.)之后,点击Ctrl + 空格键。这会打开 Visual Studio Code 的 IntelliSense:
图 2.3 – JavaScript 文件中的 IntelliSense
Visual Studio Code 只能猜测潜在的属性名称,因此它列出了它在文件中看到的变量名称和函数名称。不幸的是,在这种情况下,IntelliSense 无法提供帮助,因为正确的属性名称unitPrice并未列出。
- 现在打开
calculateTotalPrice.ts文件,从product.price中移除price,然后按Ctrl + 空格键再次打开 IntelliSense:
图 2.4 – TypeScript 文件中的 IntelliSense
这次,Visual Studio Code 列出了正确的属性。
- 从 IntelliSense 中选择unitPrice以解决类型错误。
IntelliSense 只是 TypeScript 提供的一个工具。它还可以提供强大的重构功能,例如重命名 React 组件,并帮助进行准确的代码导航,例如跳转到函数定义。
为了回顾我们在本节中学到的内容:
-
TypeScript 的类型检查功能有助于在开发过程中早期捕捉问题
-
TypeScript 使代码编辑器能够提供诸如 IntelliSense 之类的生产力功能
-
这些优势在处理大型代码库时提供了显著的好处
接下来,我们将学习 JavaScript 中的类型系统。这将进一步强调在大型代码库中使用 TypeScript 的必要性。
理解 JavaScript 类型
在理解 TypeScript 中的类型系统之前,让我们简要地探索 JavaScript 中的类型系统。为此,打开 CodeSandbox,访问codesandbox.io/,并按照以下步骤操作:
-
通过选择Vanilla选项创建一个新的纯 JavaScript 项目。
-
打开
index.js,删除其内容,并用以下代码替换:let firstName = "Fred"console.log("firstName", firstName, typeof firstName);let score = 9console.log("score", score, typeof score);let date = new Date(2022, 10, 1);console.log("date", date, typeof date);
代码将三个变量赋值为不同的值。代码还将变量值及其 JavaScript 类型输出到控制台。
这是控制台输出:
图 2.5 – 一些 JavaScript 类型
firstName是字符串,score是数字,这并不奇怪。然而,date是一个对象而不是更具体的日期类型,这有点令人惊讶。
-
让我们在现有代码之后添加几行代码:
score = "ten"console.log("score", score, typeof score);
再次,控制台输出有点令人惊讶:
图 2.6 – 变量类型变化
score变量已从number类型更改为string类型!这是因为 JavaScript 是松散类型的。
一个关键点是 JavaScript 只有一组最小的类型,如string、number和boolean。值得注意的是,所有的 JavaScript 类型都在 TypeScript 中可用,因为 TypeScript 是 JavaScript 的超集。
此外,JavaScript 允许变量改变其类型——这意味着如果变量被更改为完全不同的类型,JavaScript 引擎不会抛出错误。这种松散的类型使得代码编辑器无法捕获类型错误。
注意
关于 JavaScript 类型的更多信息,请参阅developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures。
现在我们已经了解了 JavaScript 类型系统的局限性,我们将学习 TypeScript 的类型系统,从基本类型开始。
使用基本 TypeScript 类型
在本节中,我们将首先了解 TypeScript 类型如何声明以及它们如何从赋值中推断出来。然后我们将学习 TypeScript 中常用的基本类型,这些类型在 JavaScript 中不可用,并了解它们的有用用例。
使用类型注解
TypeScript 类型注解允许变量以特定类型声明。这允许 TypeScript 编译器检查代码是否遵循这些类型。简而言之,类型注解允许 TypeScript 在代码使用错误类型的情况下比在 JavaScript 中更早地捕获错误。
打开 TypeScript Playground,访问www.typescriptlang.org/play,并按照以下步骤进行操作以探索类型注解:
-
删除左侧面板中的任何现有代码,并输入以下变量声明:
let unitPrice: number;
类型注解位于变量声明之后。它以冒号开头,后跟我们要分配给变量的类型。在这种情况下,unitPrice将被指定为number类型。请记住,number是 JavaScript 中的一个类型,这意味着它也适用于 TypeScript。
转译后的 JavaScript 如下所示:
let unitPrice;
然而,请注意类型注解已经消失。这是因为 JavaScript 中没有类型注解。
注意
您还可能在转译后的 JavaScript 顶部看到"use strict";。这意味着 JavaScript 将在 JavaScript 严格模式下执行,这将捕获更多的编码错误。有关 JavaScript 严格模式的更多信息,请参阅developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode。
-
在程序中添加第二行:
unitPrice = "Table";
注意,在这一行下方的unitPrice处出现了一条红色横线。如果您将鼠标悬停在下划线的unitPrice上,会描述一个类型错误:
图 2.7 – 捕获到的类型错误
-
您还可以使用与变量注解相同的语法为函数参数和函数的返回值添加类型注解。例如,在 TypeScript Playground 中输入以下函数:
function getTotal(unitPrice: number,quantity: number,discount: number): number {const priceWithoutDiscount = unitPrice * quantity;const discountAmount = priceWithoutDiscount * discount;return priceWithoutDiscount - discountAmount;}
我们已经声明了unitPrice、quantity和discount参数,它们都是number类型。函数的返回类型注解位于函数括号之后,在先前的例子中这也是一个number类型。
注意
我们在多个例子中使用了const和let来声明变量。let允许变量在声明后更改值,而const变量则不能更改。在先前的函数中,priceWithoutDiscount和discountAmount在初始赋值后永远不会更改值,所以我们使用了const。
-
在代码中添加另一行以调用
getTotal函数,并使用错误的quantity类型。将getTotal函数的调用结果赋值给一个具有错误类型的变量:let total: string = getTotal(500, "one", 0.1);
两个错误都会立即被检测并突出显示:
图 2.8 – 捕获到的两个类型错误
这种强类型检查是我们从 JavaScript 中得不到的,它在大型代码库中非常有用,因为它可以帮助我们立即检测类型错误。
接下来,我们将学习 TypeScript 在类型检查代码时不总是需要类型注解。
使用类型推断
类型注解非常有价值,但它们需要编写额外的代码。这些额外的代码需要花费时间来编写。幸运的是,TypeScript 强大的类型推断系统意味着类型注解并不总是需要指定。当变量被赋予一个值时,TypeScript 会推断该变量的类型。
在 TypeScript Playground 中执行以下步骤以探索类型推断:
-
首先,删除任何之前的代码,然后添加以下行:
let flag = false; -
悬停在
flag变量上。会出现一个工具提示,显示flag被推断出的类型:
图 2.9 – 悬停在变量上显示其类型
-
在此行下方添加另一行,错误地将
flag设置为无效值:flag = "table";
类型错误会立即被捕获,就像我们使用类型注解为变量分配类型时一样。
类型推断是 TypeScript 的一个优秀特性,它可以防止大量类型注解带来的代码膨胀。因此,使用类型推断并仅在推断不可行时回退到使用类型注解是一种常见的做法。
接下来,我们将查看 TypeScript 中的Date类型。
使用 Date 类型
我们已经知道 JavaScript 中不存在Date类型,但幸运的是,TypeScript 中存在Date类型。TypeScript 的Date类型是对 JavaScript Date对象的表示。
注意
有关 JavaScript Date对象的更多信息,请参阅以下链接:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date。
要探索 TypeScript 的Date类型,请在 TypeScript Playground 中执行以下步骤:
-
首先,删除任何之前的代码,然后添加以下行:
let today: Date;today = new Date();
声明了一个名为today的变量,它被分配了Date类型并设置为今天的日期。
-
将这两行重构为以下使用类型推断而不是类型注解的单行:
let today = new Date(); -
检查
today是否已被分配为Date类型,通过悬停在它上面并检查工具提示来完成:
图 2.10 – 确认 today 已推断出 Date 类型
- 现在,通过在新的行上添加
today.来检查 IntelliSense 是否正常工作:
图 2.11 – IntelliSense 在日期上工作得很好
-
删除此行并添加一条略微不同的代码行:
today.addMonths(2);
Date对象中不存在addMonths函数,因此会引发类型错误:
图 2.12 – 在日期上捕获到的类型错误
总结来说,Date类型具有我们期望的所有功能——推断、IntelliSense 和类型检查——这对于处理日期来说非常有用。
接下来,我们将了解 TypeScript 类型系统中的一个逃生口。
使用 any 类型
如果我们声明一个没有类型注解和值的变量,TypeScript 会推断出什么类型?让我们通过在 TypeScript Playground 中输入以下代码来找出答案:
let flag;
现在,将鼠标悬停在flag上:
图 2.13 – 被赋予 any 类型的变量
因此,TypeScript 给没有类型注解且没有立即赋值的变量赋予any类型。这是一种选择不执行特定变量的类型检查的方法,通常用于动态内容或第三方库中的值。然而,TypeScript 日益强大的类型系统意味着我们今天需要更少地使用any。
相反,有一个更好的选择:unknown类型。
使用unknown类型
当我们不确定类型但想以强类型方式与之交互时,我们可以使用unknown类型。执行以下步骤以探索这如何是any类型的更好替代方案:
-
在 TypeScript Playground 中,删除任何之前的代码,并输入以下内容:
fetch("https://swapi.dev/api/people/1").then((response) => response.json()).then((data) => {console.log("firstName", data.firstName);});
代码从网络 API 中获取一个《星球大战》角色。没有抛出类型错误,所以代码看起来是正常的。
- 现在点击运行选项来执行代码:
图 2.14 – firstName 属性具有未定义的值
firstName属性似乎不在获取的数据中,因为它在输出到控制台时是undefined。
为什么在引用firstName的第四行没有抛出类型错误?嗯,data的类型是any,这意味着不会对其执行类型检查。你可以悬停在data上以确认它已被赋予any类型。
-
给
data添加unknown类型注解:fetch("https://swapi.dev/api/people/1").then((response) => response.json()).then((data: unknown) => {console.log("firstName", data.firstName);});
当在firstName处引用时,现在会抛出一个类型错误:
图 2.15 – 在未知数据参数上发生类型错误
unknown类型是any类型的对立面,因为它在其类型中不包含任何内容。一个不包含任何内容的类型可能看起来没有用。然而,如果进行了检查以允许 TypeScript 扩展它,变量的类型可以被扩展。
-
在我们给 TypeScript 提供信息以扩展
data之前,将其引用的属性从firstName更改为name:fetch("https://swapi.dev/api/people/1").then((response) => response.json()).then((data: unknown) => {console.log("name", data.name);});
name是一个有效的属性,但仍然发生类型错误。这是因为data仍然是unknown。
-
现在将代码中的高亮部分更改以扩展
data类型:fetch("https://swapi.dev/api/people/1").then((response) => response.json()).then((data: unknown) => {if (isCharacter(data)) {console.log("name", data.name);}});function isCharacter(character: any): character is { name: string } {return "name" in character;}
if语句使用一个名为isCharacter的函数来验证对象中是否包含name属性。在这个例子中,这个调用的结果是true,所以逻辑将流入if分支。
注意isCharacter的返回类型,它是:
character is { name: string }
如果函数返回true,则这是一个character到{ name: string }的类型。在这个例子中,类型谓词是true,所以character被扩展为一个具有name字符串属性的对象。
- 在引用
data变量的每一行上悬停。data最初具有unknown类型,其中它被赋予了一个类型注解。然后,在if分支内部,它被扩展到{name: string}:
图 2.16 – 分配给数据的扩展类型
注意到类型错误也已经消失了。太好了!
- 接下来,运行代码。你将在控制台看到
Luke Skywalker输出。
总结来说,unknown类型是对于不确定数据类型的优秀选择。然而,你不能与unknown变量交互——变量必须在任何交互之前被扩展到其他类型。
接下来,我们将学习一个用于函数不返回值的类型。
使用 void 类型
void类型用于表示函数的返回类型,在这种情况下,函数不返回任何值。
例如,在 TypeScript Playground 中输入以下函数:
function logText(text: string) {
console.log(text);
}
悬停在函数名上可以确认函数的返回类型被赋予了一个void类型。
图 2.17 – 返回类型已确认为准 void
你可能认为你可以使用undefined作为前面示例的返回类型:
function logText(text: string): undefined {
console.log(text);
}
然而,这引发了一个类型错误,因为undefined类型的返回类型意味着函数预期会返回一个值(类型为undefined)。示例函数没有返回任何值,所以返回类型是void。
总结来说,void是一个特殊类型,用于函数的返回类型,在这种情况下,函数没有返回语句。
接下来,我们将学习never类型。
使用 never 类型
never类型表示永远不会发生的事情,通常用于指定不可达的代码区域。让我们在 TypeScript Playground 中探索一个例子:
-
删除任何现有代码并输入以下代码:
function foreverTask(taskName: string): never {while (true) {console.log(`Doing ${taskName} over and over again ...`);}}
函数调用了一个无限循环,这意味着函数永远不会退出。因此,我们给函数赋予了一个never类型的返回类型注解,因为我们不期望函数会退出。这与void不同,因为void意味着它将会退出,但没有返回值。
注意
在前面的例子中,我们使用 JavaScript 模板字符串来构建输出到控制台的字符串。模板字符串由反引号(` `)包围,并可以包含以美元符号($${expression})为前缀的花括号中的 JavaScript 表达式。当需要将静态文本与变量合并时,模板字符串非常出色。有关模板字符串的更多信息,请参阅此链接:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals。
-
将
foreverTask函数更改为跳出循环:function foreverTask(taskName: string): never {while (true) {console.log(`Doing ${taskName} over and over again ...`);break;}}
TypeScript 正确地抱怨:
图 2.18 – never 返回类型上的类型错误
-
删除
break语句并删除never返回类型注解:function foreverTask(taskName: string) {while (true) {console.log(`Doing ${taskName} over and over again ...`);}} -
将鼠标悬停在
foreverTask函数名称上。我们可以看到 TypeScript 推断的返回类型为void:
图 2.19 – 返回类型推断为 void
因此,TypeScript 在这种情况下无法推断出 never 类型。相反,它推断出返回类型为 void,这意味着函数将不带任何值退出,但这在本例中并不适用。这是一个提醒,始终要检查推断的类型,并在适当的地方使用类型注解。
总结来说,never 类型用于代码永远不会到达的地方。
接下来,让我们来介绍数组。
使用数组
数组是 TypeScript 从 JavaScript 继承的结构。我们像往常一样向数组添加类型注解,但在末尾使用方括号 [] 来表示这是一个数组类型。
让我们在 TypeScript Playground 中探索一个示例:
-
删除任何现有代码,并输入以下内容:
const numbers: number[] = [];
或者,可以使用 Array 泛型类型语法:
const numbers: Array<number> = [];
我们将在 第十一章 可重用组件 中学习 TypeScript 中的泛型。
-
通过使用数组的
push函数向数组中添加1:numbers.push(1); -
现在向数组中添加一个字符串:
numbers.push("two");
正如我们所预期的那样,会抛出一个类型错误:
图 2.20 – 向数字数组添加字符串类型时的类型错误
-
现在将所有代码替换为以下内容:
const numbers = [1, 2, 3]; -
将鼠标悬停在
numbers上以验证 TypeScript 已经推断出它的类型为number[]。
图 2.21 – 数组类型推断
极好 – 我们可以看到 TypeScript 的类型推断在数组上是如何工作的!
数组是用于结构化数据最常用的类型之一。在前面的示例中,我们只使用了一个具有 number 类型元素的数组,但可以使用任何类型作为元素,包括具有自己属性的对象。
在本节中,我们回顾了所有基本类型:
-
TypeScript 向 JavaScript 类型添加了许多有用的类型,例如
Date,并且能够表示数组。 -
TypeScript 可以从其分配的值推断出变量的类型。当类型推断无法给出所需类型时,可以使用类型注解。
-
具有类型
any的变量不会进行类型检查,因此应避免使用此类型。 -
unknown类型是any的强类型替代品,但unknown变量必须进行类型提升才能进行交互。 -
void是一个不返回值的函数的返回类型。 -
never类型可以用来标记代码中无法到达的区域。 -
可以在数组项目类型之后使用方括号来定义数组类型。
在下一节中,我们将学习如何创建自己的类型。
创建 TypeScript 类型
上一节展示了 TypeScript 拥有一套非常出色的标准类型。在本节中,我们将学习如何创建我们自己的类型。我们将从学习创建对象类型的三个不同方法开始。然后,我们将学习关于强类型 JavaScript 类的内容。最后,我们将学习两种创建用于存储一系列值变量的类型的方法。
使用对象类型
对象在 JavaScript 程序中非常常见,因此学习如何在 TypeScript 中表示它们非常重要。实际上,我们已经在本章前面使用对象类型为calculateTotalPrice函数中的product参数创建了一个对象类型。以下是product参数类型注解的提醒:
function calculateTotalPrice(
product: { name: string; unitPrice: number },
...
) {
...
}
TypeScript 中的对象类型表示得有点像 JavaScript 对象字面量。然而,与属性值不同,属性类型被指定。对象定义中的属性可以用分号或逗号分隔,但使用分号是常见做法。
清除 TypeScript Playground 中的任何现有代码,并按照以下示例来探索对象类型:
-
将以下变量赋值给一个对象:
let table = {name: "Table", unitPrice: 450};
如果将鼠标悬停在table变量上,你会看到它被推断为以下类型:
{
name: string;
unitPrice: number;
}
因此,类型推断在对象中工作得很好。
-
现在,在下一行,尝试将
discount属性设置为10:table.discount = 10;
尽管类型中不存在discount属性,但只有name和unitPrice属性存在。因此,发生类型错误。
-
假设我们想要表示一个包含
name和unitPrice属性的product对象,但希望unitPrice是可选的。删除现有代码,并用以下代码替换:const table: { name: string; unitPrice: number } = {name: "Table",}; -
这会引发类型错误,因为
unitPrice在类型注解中是一个必需的属性。我们可以使用以下?符号来使其可选而不是必需:const table: { name: string; unitPrice?: number } = {name: "Table",};
类型错误消失了。
注意
在函数中可以使用?符号表示可选参数。例如,myFunction(requiredParam: string, optionalParam: string)。
现在,让我们学习一种简化对象类型定义的方法。
创建类型别名
我们在上一个示例中使用的类型注解相当长,对于更复杂的对象结构会更长。此外,必须为不同的变量写入相同的对象结构会有些令人沮丧:
const table: { name: string; unitPrice?: number } = ...;
const chair: { name: string; unitPrice?: number } = ...;
类型别名解决了这些问题。正如其名所示,类型别名指的是另一个类型,其语法如下:
type YourTypeAliasName = AnExistingType;
打开 TypeScript Playground 并跟随示例来探索类型别名:
-
首先,为我们在上一个示例中使用的商品对象结构创建一个类型别名:
type Product = { name: string; unitPrice?: number }; -
现在,将两个变量分配给这个
Product类型:let table: Product = { name: "Table" };let chair: Product = { name: "Chair", unitPrice: 40 };
这样就干净多了!
-
类型别名可以使用
&符号扩展另一个对象。通过添加以下类型别名创建一个折扣产品的第二个类型:type DiscountedProduct = Product & { discount: number };
DiscountedProduct表示一个包含name、unitPrice(可选)和discount属性的对象。
注意
使用&符号扩展另一个类型的类型被称为交集类型。
-
按照以下方式添加以下变量,使用
DiscountedProduct类型:let chairOnSale: DiscountedProduct = {name: "Chair on Sale",unitPrice: 30,discount: 5,}; -
类型别名也可以用来表示函数。添加以下类型别名来表示一个函数:
type Purchase = (quantity: number) => void;
前面的类型表示一个包含number参数的函数,并且不返回任何内容。
-
使用
Purchase类型在Product类型中创建purchase函数属性,如下所示:type Purchase = (quantity: number) => void;type Product = {name: string;unitPrice?: number;purchase: Purchase;};
因为需要purchase函数属性,table、chair和chairOnSale变量声明将引发类型错误。
-
按照以下方式向
table变量声明中添加purchase函数属性:let table: Product = {name: "Table",purchase: (quantity) =>console.log(`Purchased ${quantity} tables`),};table.purchase(4);
table变量声明上的类型错误已解决。
-
可以以类似
chair和chairOnSale变量声明的方式添加purchase属性来解决这个问题。然而,在这个探索中忽略这些类型错误,继续到下一步。 -
点击运行选项来运行购买四张桌子的代码。**“已购买 4 张桌子”**输出到控制台。
总结来说,类型别名允许将现有类型组合在一起,并提高类型的可读性和可重用性。我们将在本书中广泛使用类型别名。
接下来,我们将探索创建类型的另一种方法。保持 TypeScript Playground 打开,代码保持不变——我们将在下一节中使用它。
创建接口
正如我们在上一个示例中使用类型别名创建的那样,可以使用 TypeScript 的interface关键字创建对象类型,后跟其名称,然后是括号中组成interface的部分:
interface Product {
...
}
前往包含类型别名探索中代码的 TypeScript Playground,并跟随操作来探索接口:
-
首先,将
Product类型别名替换为以下Product接口:interface Product {name: string;unitPrice?: number;}
table变量赋值出现类型错误,因为purchase属性尚未存在——我们将在第 4 步中添加它。然而,chair变量赋值编译时没有错误。
-
接口可以使用
extends关键字扩展另一个接口。将DiscountedProduct类型别名替换为以下接口:interface DiscountedProduct extends Product {discount: number;}
注意到chairOnSale变量赋值编译时没有错误。
-
接口也可以用来表示函数。添加以下接口来表示一个函数,替换类型别名版本:
interface Purchase {(quantity: number): void}
创建函数的接口语法不如使用类型别名直观。
-
按照以下方式将
Purchase接口添加到Product接口中:interface Product {name: string;unitPrice?: number;purchase: Purchase;}
table变量声明上的类型错误已解决,但现在在chair和chairOnSale变量声明上引发了类型错误。
- 点击运行选项来运行购买四张桌子的代码。**“已购买 4 张桌子”**输出到控制台。
在前面的步骤中,我们使用接口和使用类型别名执行了相同的任务。因此,显而易见的问题是,我应该何时使用类型别名而不是接口,反之亦然? 类型别名和接口在创建对象类型方面的功能非常相似——所以简单的答案是,这取决于对对象类型的偏好。然而,类型别名可以创建接口无法创建的类型,例如联合类型,我们将在本章后面介绍。
注意
有关类型别名和接口之间差异的更多信息,请参阅以下链接:www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces。
本书其余部分使用类型别名而不是接口来定义类型。
接下来,我们将学习如何使用 TypeScript 与类一起使用。
创建类
类是标准的 JavaScript 功能,它作为创建对象的模板。在类中定义的属性和方法将自动包含在从该类创建的对象中。
打开 TypeScript Playground,删除任何现有代码,并按照以下步骤执行以探索 TypeScript 中的类:
-
添加以下代码以创建一个表示产品并具有名称和单价属性的类的示例:
class Product {name;unitPrice;}
如果你悬停在 name 和 unitPrice 属性上,你会看到它们具有 any 类型。正如我们所知,这意味着不会对它们进行类型检查。
-
将以下类型注解添加到属性中:
class Product {name: string;unitPrice: number;}
不幸的是,TypeScript 抛出了以下错误:
图 2.22 – 类属性上的类型错误
错误是因为当创建类的实例时,这些属性值将是 undefined,这不在 string 或 number 类型中。
-
一种解决方案是使属性可选,以便它们可以接受
undefined作为值。通过在类型注解的开始处添加?符号来尝试此解决方案:class Product {name?: string;unitPrice?: number;} -
如果我们不希望值最初是
undefined,我们可以像这样分配初始值:class Product {name = "";unitPrice = 0;}
如果你现在悬停在属性上,你会看到 name 已推断为 string 类型,而 unitPrice 已推断为 number 类型。
-
向类属性添加类型的另一种方法是在构造函数中。移除分配给属性的值,并将构造函数添加到类中,如下所示:
class Product {name;unitPrice;constructor(name: string, unitPrice: number) {this.name = name;this.unitPrice = unitPrice;}}
如果你悬停在属性上,你会看到已推断出正确的类型。
-
实际上,如果构造函数参数被标记为
public,则不需要定义属性。class Product {constructor(public name: string, public unitPrice: number) {this.name = name;this.unitPrice = unitPrice;}}
TypeScript 会自动为标记为 public 的构造函数参数创建属性。
-
可以像我们之前为函数所做的那样,将类型注解添加到方法参数和返回值中:
class Product {constructor(public name: string, public unitPrice: number) {this.name = name;this.unitPrice = unitPrice;}getDiscountedPrice(discount: number): number {return this.unitPrice - discount;}} -
现在创建类的实例并将它的折扣价格输出到控制台:
const table = new Product("Table", 45);console.log(table.getDiscountedPrice(5));
如果运行代码,40会被输出到控制台。
总结一下,类属性可以在构造函数中或通过分配默认值来指定类型。类方法可以像常规 JavaScript 函数一样强类型化。
注意
更多关于类的信息,请参阅以下链接:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes。
接下来,我们将学习如何创建一个表示一系列值的类型。
创建枚举
enum关键字,后面跟着我们想要给它的名字,然后是其可能的值,用大括号括起来。
让我们在 TypeScript Playground 中探索一个例子:
-
首先,创建包含
Low、Medium和High值的Level枚举:enum Level {Low,Medium,High} -
现在,创建一个
level变量,并将其赋值为Level枚举中的Low和High值。也将level值输出到控制台:let level = Level.Low;console.log(level);level = Level.Highconsole.log(level);
注意,当你引用枚举时,你会得到智能感知。
- 点击运行选项来执行代码并观察枚举值:
图 2.23 – 枚举值的输出
默认情况下,枚举是零基数字(这意味着第一个枚举值是 0,下一个是 1,再下一个是 2,依此类推)。在先前的例子中,Level.Low是0,Level.Medium是1,Level.High是2。
-
而不是使用默认值,我们可以在等于(
=)符号之后显式地为每个枚举项定义自定义值。显式地将值设置为1到3之间:enum Level {Low = 1,Medium = 2,High = 3}
你可以重新运行代码来验证这一点。
-
现在,让我们做一些有趣的事情。将
level赋值为大于 3 的数字:level = 10;
注意,这里没有发生类型错误。这有点令人惊讶——基于数字的枚举并不像我们希望的那样类型安全。
-
而不是使用数字枚举值,让我们尝试使用字符串。将所有当前代码替换为以下内容:
enum Level {Low = "L",Medium = "M",High = "H"}let level = Level.Low;console.log(level);level = Level.Highconsole.log(level);
如果运行此代码,我们会看到预期的L和H输出到控制台。
-
添加另一行代码,将
level赋值为以下字符串:level = "VH";level = "M"
我们立即在这些赋值上看到类型错误被抛出:
图 2.24 – 确认字符串枚举是类型安全的
总结一下,枚举是一种用用户友好的名称表示一系列值的方法。默认情况下,它们是零基数字,并不像我们希望的那样类型安全。然而,我们可以将枚举基于字符串,这更类型安全。
接下来,我们将学习 TypeScript 中的联合类型。
创建联合类型
联合类型是多个其他类型的数学并集,用于创建一个新类型。与枚举类似,联合类型可以表示一系列值。如前所述,可以使用类型别名来创建联合类型。
联合类型的一个例子如下:
type Level = "H" | "M" | "L";
这个 Level 类型与我们之前创建的 Level 类型的枚举版本类似。区别在于,联合类型只包含值("H","M","L")而不是名称("High","Medium","Large")和值。
清除 TypeScript Playground 中的任何现有代码,让我们来玩一玩联合类型:
-
首先创建一个表示
"red","green"或"blue"的类型:type RGB = "red" | "green" | "blue";
注意,这个类型是字符串的联合,但联合类型可以由任何类型组成——甚至可以是混合类型!
-
创建一个具有
RGB类型的变量并分配一个有效值:let color: RGB = "red"; -
现在尝试分配一个类型外的值:
color = "yellow";
如预期,发生类型错误:
图 2.25 – 联合类型上的类型错误
当一个类型只能持有特定的一组字符串时,如前例所示,由字符串组成的联合类型非常出色。
这里是对我们关于创建类型的了解的回顾:
-
对象和函数可以使用类型别名或接口来表示。它们具有非常相似的功能,但类型别名语法在表示函数时更为直观。
-
?符号可以指定对象属性或函数参数是可选的。 -
可以将类型注解添加到类属性、构造函数和方法参数中,以使它们具有类型安全性。
-
与基于字符串的联合类型类似,基于字符串的枚举非常适合一组特定的字符串。如果字符串具有意义,那么字符串联合类型是最简单的方法。如果字符串没有意义,则可以使用字符串枚举来使它们可读。
现在我们已经涵盖了类型,接下来,我们将学习 TypeScript 编译器。
使用 TypeScript 编译器
在本节中,我们将学习如何使用 TypeScript 编译器进行代码类型检查并将其转换为 JavaScript。首先,我们将使用 Visual Studio Code 创建一个包含我们在上一节中编写的代码的简单 TypeScript 项目。然后,我们将使用 Visual Studio Code 内部的终端与 TypeScript 编译器进行交互。
在您选择的空白文件夹中打开 Visual Studio Code,并执行以下步骤:
-
在包含以下内容的
package.json中:{"name": "tsc-play","dependencies": {"typescript": "⁴.6.4"},"scripts": {"build": "tsc src/product.ts"}}
该文件定义了一个项目名称为 tsc-play,并将 TypeScript 设置为唯一的依赖项。该文件还定义了一个名为 build 的 npm 脚本,它将调用 TypeScript 编译器(tsc),并将 src 文件夹中的 product.ts 文件传递给它。不要担心 product.ts 文件不存在——我们将在 步骤 3 中创建它。
-
现在,通过从 终端 菜单中选择 New Terminal 来打开 Visual Studio Code 终端,然后输入以下命令:
npm install
这将安装 package.json 的 dependencies 部分中列出的所有库。因此,这将安装 TypeScript。
-
创建一个名为
src的文件夹,然后在其中创建一个名为product.ts的文件。 -
打开
product.ts并添加以下内容:class Product {constructor(public name: string, public unitPrice: number) {this.name = name;this.unitPrice = unitPrice;}getDiscountedPrice(discount: number): number {return this.unitPrice - discount;}}const table = new Product("Table", 45);console.log(table.getDiscountedPrice(5));
这段代码可以在使用类的部分中找到。您可以从github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition/blob/main/Chapter2/Section4-Using-the-compiler/src/product.ts复制此代码。
-
在终端中输入以下命令:
npm run build
这将运行我们在第一步中定义的 npm build 脚本。
命令完成后,注意在src文件夹中product.ts旁边会出现一个product.js文件。
-
打开转换后的
product.js文件并阅读内容。它看起来如下所示:var Product = /** @class */ (function () {function Product(name, unitPrice) {this.name = name;this.unitPrice = unitPrice;this.name = name;this.unitPrice = unitPrice;}Product.prototype.getDiscountedPrice = function (discount) {return this.unitPrice - discount;};return Product;})();var table = new Product("Table", 45);console.log(table.getDiscountedPrice(5));
注意到类型注解已被删除,因为它们不是有效的 JavaScript。同时注意,它已被转换为 JavaScript,能够在非常旧的浏览器中运行。
TypeScript 编译器使用的默认配置并不理想。例如,我们可能希望将转换后的 JavaScript 放在一个完全独立的文件夹中,并且可能希望针对更新的浏览器。
-
可以使用名为
tsconfig.json的文件来配置 TypeScript 编译器。在项目的根目录中添加一个tsconfig.json文件,包含以下代码:{"compilerOptions": {"outDir": "build","target": "esnext","module": "esnext","lib": ["DOM", "esnext"],"strict": true,"jsx": "react","moduleResolution": "node","noEmitOnError": true},"include": ["src/**/*"],"exclude": ["node_modules", "build"]}
下面是compilerOptions字段中每个设置的说明:
-
outDir:这是放置转换后的 JavaScript 的文件夹。 -
target:这是我们想要转换到的 JavaScript 版本。esnext目标意味着下一个版本。 -
Module:这是代码中使用的模块类型。esnext模块意味着标准 JavaScript 模块。 -
Lib:在类型检查过程中包含的标准库类型。DOM提供浏览器 DOM API 类型,而esnext是 JavaScript 下一个版本 API 的类型。 -
Strict:当设置为true时,表示最严格的类型检查级别。 -
Jsx:当设置为React时,允许编译器转换 React 的 JSX。 -
moduleResolution:这是查找依赖项的方式。我们希望 TypeScript 在node_modules文件夹中查找,因此我们选择了node。 -
noEmitOnError:当设置为true时,表示如果发现类型错误,则不会发生转换。
include字段指定要编译的 TypeScript 文件,而exclude字段指定要排除的文件。
注意
关于 TypeScript 编译器选项的更多信息,请参阅以下链接:www.typescriptlang.org/tsconfig。
-
TypeScript 编译器配置现在指定了
src文件夹中的所有文件都要进行编译。因此,从package.json中的build脚本中删除文件路径:{...,"scripts": {"build": "tsc"}} -
删除
src文件夹中之前的转换后的product.js文件。 -
在终端中重新运行
build命令:npm run build
这次转换的文件被放置在 build 文件夹中。您还会注意到,现在转换的 JavaScript 使用了现代浏览器支持的类。
-
我们将要尝试的最后一件事是类型错误。打开
product.ts并更新构造函数以引用错误的属性名:class Product {constructor(public name: string, public unitPrice: number) {this.name = name;this.price = unitPrice;}...} -
删除
build文件夹以移除之前转换的 JavaScript 文件。 -
在终端中重新运行
build命令:npm run build
类型错误在终端中报告。注意,JavaScript 文件没有被转换。
总结来说,TypeScript 有一个名为 tsc 的编译器,我们可以使用它来执行类型检查和转换,作为持续集成过程的一部分。编译器非常灵活,可以使用名为 tsconfig.json 的文件进行配置。值得注意的是,Babel 通常用于转换 TypeScript(以及 React),让 TypeScript 专注于类型检查。
接下来,我们将回顾本章所学的内容。
摘要
TypeScript 通过丰富的类型系统补充了 JavaScript,在本章中,我们通过使用 TypeScript 的类型检查来早期捕获错误。
我们还了解到,JavaScript 类型,如 number 和 string,可以在 TypeScript 中使用,以及仅存在于 TypeScript 中的类型,如 Date 和 unknown。
我们探讨了联合类型,并了解到这些类型非常适合表示一组特定的字符串。我们现在明白,如果字符串值不是非常有意义,字符串枚举是字符串联合类型的替代方案。
可以使用类型别名创建新类型。我们了解到类型别名可以基于对象、函数,甚至是联合类型。我们现在知道,类型注解中的 ? 符号使对象属性或函数参数成为可选的。
我们还了解了很多关于 TypeScript 编译器及其如何在不同用例中良好工作的信息,因为它非常可配置。当我们开始在下一章中使用 TypeScript 与 React 一起工作时,这将是重要的。在那里,我们将在学习如何为 React 和 TypeScript 项目设置不同的方式之前,学习如何为 React props 和 state 设置强类型。
问题
回答以下问题以检查您对 TypeScript 的了解:
-
在以下代码中,
flag变量的推断类型会是什么?let flag = false; -
以下函数的返回类型是什么?
function log(message: string) {return console.log(message);} -
日期数组的类型注解是什么?
-
在以下代码中会发生类型错误吗?
type Point = {x: number; y: number; z?: number};const point: Point = { x: 24, y: 65 }; -
使用类型别名创建一个只能持有介于 1 和 3 之间(包括 1 和 3)的整数值的数字。
-
当发现类型错误时,可以使用哪个 TypeScript 编译器选项来防止转换过程?
-
以下代码会引发类型错误,因为
lastSale不能接受null值:type Product = {name: string;lastSale: Date;}const table: Product = {name: "Table", lastSale: null}
如何更改 Product 类型以允许 lastSale 接受 null 值?
答案
-
flag变量会被推断为boolean类型。 -
函数中的返回类型是
void。 -
日期数组可以表示为
Date[]或Array<Date>。 -
在
point变量上不会引发类型错误。因为它可选,所以不需要包含z属性。 -
可以创建一个用于数字 1-3 的类型,如下所示:
type OneToThree = 1 | 2 | 3; -
当发现类型错误时,可以使用
noEmitOnError编译器选项(设置为true)来防止编译过程。 -
联合类型可用于
lastSale属性,以便它接受null值:type Product = {name: string;lastSale: Date | null;}const table: Product = {name: "Table", lastSale: null}