TypeScript 技巧和窍门





5.00/5 (1投票)
TypeScript 如何改进 JavaScript 编码。
引言
TypeScript 是 JavaScript 的超集。它就像 JavaScript,但拥有超能力。
原生 JavaScript 是一种动态类型语言。例如,如果你给一个变量赋值为数字类型,然后在代码的后面又给同一个变量赋值为字符串,JavaScript 仍然可以正常编译。你只会在生产环境出现问题时才收到错误。
如果你反对使用 TypeScript 等静态类型工具,你可以使用 JavaScript lint 工具来提供一些类型检查。lint 工具可以帮助我们发现示例中的错误。然而,lint 工具也有其局限性。例如,lint 工具不支持联合类型——我们将在文章中进一步探讨这一点——它们也无法对复杂的对象结构进行 lint 检查。
TypeScript 为 JavaScript 提供了更多类型功能,使你能够更好地组织代码库。它在编译时对你的代码库进行类型检查,并有助于防止可能进入生产环境的错误。
TypeScript 以许多不同的方式改进了 JavaScript 开发。但在本文中,我们将重点关注六个方面,包括使用 TypeScript 的实用提示和技巧。其中一些提示将讨论 TypeScript 支持函数式编程的方式。
只读类型
函数式编程通常要求不可变变量——以及扩展的不可变对象。一个具有四个属性的对象在其整个生命周期中必须具有相同的属性,并且这些属性的值在任何时候都不能改变。
TypeScript 使用 Readonly 实用工具类型使其成为可能。下面是一个没有它的类型
...
type Product = {
name: string
price: number
}
const products: Product[] = [
{
name: "Apple Watch",
price: 400,
},
{
name: "Macbook",
price: 1000,
},
]
products.forEach(product => {
// mutating here
product.price = 500
})
...
在代码中,我们修改了 `price` 属性。由于新值是数字数据类型,TypeScript 不会抛出错误。但是使用 `Readonly`,我们的代码如下
...
const products: Readonly<Product>[] = [
{
name: "Apple Watch",
price: 400,
},
{
name: "Macbook",
price: 1000,
},
]
products.forEach(product => {
// mutating here
product.price = 500
})
...
正如我们在此屏幕截图中看到的,`price` 属性是只读的,不能赋值为另一个值。尝试修改此对象的值将抛出错误。
联合类型
TypeScript 允许你以引人注目的方式组合类型。两种或多种类型的组合称为联合类型。你使用“`|`”符号创建联合类型。让我们看一些例子。
数字和字符串联合类型
有时,你需要一个变量是数字。其他时候,你需要同一个变量是 `string`。
使用 TypeScript,你可以通过以下简单方法实现这一点
function(id: number | string) {
...
}
这里声明的联合类型的挑战在于你不能调用 `id.toUpperCase`,因为 TypeScript 不知道你是在函数声明期间传递字符串还是数字。因此,要使用 `toUpperCase` 方法,你必须使用 `typeof === "string"` 检查 `id` 是否为 `string`。
但是,如果它不能将标准方法应用于构成特定联合的所有成员,TypeScript 就不会报错。
限制可接受类型
通过联合,你还可以限制变量的可接受数据类型值。你使用字面量类型来做到这一点。下面是一个例子
function(type: "picture" | "video") {
...
}
此联合类型包含图片和视频的字符串字面量类型。此代码会导致其他字符串值抛出错误。
可辨识联合
联合的另一个优点是你可以拥有不同结构的对象类型,每个类型都具有一个共同的区分属性。下面是一个例子
...
type AppleFruit = {
color: string;
size: "small" | "large"
}
type OrangeFruit = {
isRipe: boolean;
count: number;
}
function describeFruit(fruit: AppleFruit | OrangeFruit) {
...
}
...
在此代码中,我们有一个联合类型 `fruit`,由两种不同的对象类型组成:`AppleFruit` 和 `OrangeFruit`。这两种水果类型没有共同属性。这种差异使得 TypeScript 在我们使用 `fruit` 时很难知道它是哪种水果,如下面的代码和屏幕截图所示
...
function describeFruit(fruit: AppleFruit | OrangeFruit) {
if (fruit.color) {
// throw error...see Figure B.
}
}
...
此屏幕截图中的错误显示 `color` 在 `orange` 类型上不存在。有两种解决方案。
第一个解决方案是更可接受地检查 `color` 属性是否存在。方法如下
...
function describeFruit(fruit: AppleFruit | OrangeFruit) {
if ("color" in fruit) {
// now typescript knows fruit is of the apple type
}
}
...
我们检查 `color` 属性是否存在于 `fruit` 对象中。使用此检查,TypeScript 可以正确推断类型,如屏幕截图所示
第二个解决方案是使用可辨识联合。此方法意味着有一个属性可以清楚地区分这两个对象。TypeScript 可以使用该属性来了解在特定时间正在使用哪种类型。方法如下
...
type AppleFruit = {
name: "apple";
color: string;
size: "small" | "large";
}
type OrangeFruit = {
name: "orange";
isRipe: boolean;
count: number;
}
function describeFruit(fruit: AppleFruit | OrangeFruit) {
if (fruit.name === "apple") {
// apple type detected
}
}
...
由于两种类型都具有 `name` 属性,因此 `fruit.name` 不会抛出错误。而且,使用 `name` 属性的值,TypeScript 可以确定 `fruit` 的类型。
交叉类型
与涉及 `type1`、`type2` _或_ `type3` 的联合类型相反,交叉类型是 `type1`、`type2` _和_ `type3`。
这些类型之间的另一个显著区别是,虽然联合类型可以是字符串或数字,但交叉类型不能同时是字符串和数字。数据不能同时是字符串和数字。因此,交叉类型涉及对象。
既然我们已经讨论了联合类型和交叉类型之间的区别,那么让我们探讨一些进行交叉的方法
...
interface Profile {
name: string;
phone: string;
}
interface AuthCreds {
email: string;
password: string;
}
interface User: Profile & AuthCreds
...
`Profile` 和 `AuthCreds` 是彼此独立存在的接口类型的示例。这种独立性意味着你可以创建 `Profile` 类型的对象和 `AuthCreds` 类型的另一个对象,并且这些对象可能彼此不相关。但是,你可以交叉这两种类型以创建更大的类型:`User`。此类型的结构是一个具有四个属性的对象:`name`、`phone`、`email` 和 `password`,所有这些属性都是字符串类型。
现在你可以像这样创建一个 `User` 对象
...
const user:User = {
name: "user";
phone: "222222",
email: "user@user.com"
password: "***"
}
...
TypeScript 泛型
有时,当你创建函数时,你知道它的返回类型。下面是一个例子
...
interface AppleFruit {
size: number
}
interface FruitDescription {
description: string;
}
function describeFruit(fruit: AppleFruit): AppleFruit & FruitDescription {
return {
...fruit,
description: "A fruit",
}
}
const fruit: AppleFruit = {
size: 50
}
describeFruit(fruit)
...
在此示例中,`describeFruit` 函数接受 `AppleFruit` 类型的 `fruit` 参数。它返回由 `AppleFruit` 和 `FruitDescription` 类型组成的交叉类型。
但是,如果你希望此函数返回不同水果类型的描述怎么办?泛型在这里很相关。下面是一个例子
...
interface AppleFruit {
size: number
}
interface OrangeFruit {
isOrangeColor: boolean;
}
interface FruitDescription {
description: string;
}
function describeFruit<T>(fruit: T): T & FruitDescription {
return {
...fruit,
description: "A fruit",
}
}
const appleFruit: AppleFruit = {
size: 50
}
describeFruit(appleFruit)
const orangeFruit: OrangeFruit = {
isOrangeColor: true
}
describeFruit<OrangeFruit>(orangeFruit)
...
泛型函数 `describeFruit` 接受不同的类型。代码在调用函数时确定要传递的水果类型。
第一次调用 `describeFruit` 时,TypeScript 会自动推断 `T` 为 `AppleFruit`,因为 `appleFruit` 是该类型的。
下一次,我们在调用函数之前使用“`OrangeFruit`”将 `T` 的类型指定为 `OrangeFruit`。
这些行执行相同的操作,但在某些情况下,自动推断可能不准确。
在我们的示例中,我们可以向该函数传递不同的类型,它只是返回 `FruitDescription` 和我们传递的类型的交叉。
下面是一个使用泛型将类型传递给函数的示例
`describeFruit` 函数有一个类型,正如我们最初将其定义为 `OrangeFruit`。
使用 TypeScript 的路径别名
你通常可能会像这样 `import`
...
import Button from "../../../../components/Button"
...
此 `import` 命令可能位于需要此组件的不同文件中。当你更改“_Button_”文件的位置时,你还需要更改使用它的各种文件中的此 `import` 行。这种调整还会导致版本控制中需要跟踪更多文件更改。
我们可以通过使用别名路径来改进导入方式。
TypeScript 使用“tsconfig.json”文件来存储使其按你希望的方式工作的配置。在其中,有一个 paths 属性。此属性允许你为应用程序中的不同目录设置路径别名。下面是它使用 compilerOptions、baseUrl 和 paths 的样子
...
{
"compilerOptions": {
"baseUrl": ".", // required if "paths" is specified.
"paths": {
"components/*": ["./src/components/*"] // path is relative to the baseUrl
}
}
}
...
使用组件别名,你现在可以像这样导入
...
import Button from "components/Button"
...
无论你在目录中有多深,使用此命令都可以正确解析“_Button_”文件。
现在,当你更改组件的位置时,你所要做的就是更新 `paths` 属性。此方法意味着更一致的文件和更少的版本控制文件更改。
使用具有内置 TypeScript 支持的库
当你使用没有 TypeScript 支持的库时,你会错过 TypeScript 的优点。即使是一个简单的错误——例如为参数或对象属性使用错误的数据类型——如果没有 TypeScript 提供的警告,也可能会让你头疼。如果没有此警报,你的应用程序可能会崩溃,因为它期望字符串,但你却传递了数字。
并非每个库都支持 TypeScript。当你安装_确实_支持 TypeScript 的库时,它会安装带有 TypeScript 声明文件的分布式代码。
结论
在本文中,我们探讨了 TypeScript 如何改进 JavaScript 编码。我们还讨论了 TypeScript 如何支持一些函数式编程技术。这种能力使 TypeScript 适用于面向对象编程 (OOP) 开发人员和函数式程序员。
下一步,你可以深入研究 TypeScript 的 配置选项。TypeScript 提供了这个详尽的资源来帮助你构建下一个应用程序。
历史
- 2022 年 1 月 17 日:初始版本