65.9K
CodeProject 正在变化。 阅读更多。
Home

TypeScript 技巧和窍门

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2022年1月17日

CPOL

7分钟阅读

viewsIcon

6543

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 属性。此属性允许你为应用程序中的不同目录设置路径别名。下面是它使用 compilerOptionsbaseUrlpaths 的样子

...  
{  
    "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 日:初始版本
© . All rights reserved.