理解 TypeScript 的优点和缺陷

图片

JavaScript的动态类型允许灵活性,但它增加了额外的复杂性和风险。如果有人将 Number 传递给一个期望 Date 的函数,函数很可能会抛出异常,除非函数添加一些额外的代码来确保参数实际上是 Date 。

TypeScript 的主要优势在于类型检查,通过在语言中添加静态类型检查,我们可以在构建时捕获许多这样的问题,从而在代码发布之前修复它们。

但是它并不是万灵药,就像任何工具一样,它也有积极和消极的方面。

  • 好的方面

    • 优秀的代码完成

    • 支持渐进式采用

    • 更好的第三方库集成

    • 社区提供的类型

  • 不太好的方面

    • 安全性未得到保证(运行时)

    • TypeScript 增加了额外的复杂性,即使是简单的任务

    • 错误消息可能难以解读

    • 构建性能可能会受到影响

    • 这不是万无一失的

优点
 

让我们从探索 TypeScript 的优点开始,这里没有列出其他优点,但这些是其中最好的一些。

优秀的代码自动补全
 

我主要使用 Visual Studio Code 作为我的 IDE。在它的许多强大功能中,VS Code 内置了 TypeScript 智能弹出代码补全可用于 web 平台 API 以及任何具有类型定义的第三方包。

不记得拼接数组的参数了?VS Code 帮你解决了。

const numbers = [1, 2, 3];

当我开始调用  numbers  数组上的函数时,VS Code的智能感知开始工作,并向我显示匹配的函数:

图片

我看到了函数签名和每个参数的描述。当我继续输入对  splice  的调用时,当前参数被高亮显示:

图片

如果我调用函数错误,我立即得到红色下划线:

图片

当然,VS Code 只是一个编辑器,许多其他的现代编辑器和 IDE 也提供了一流的 TypeScript 支持。

支持渐进式采用
 

TypeScript 有很多配置选项,包括许多控制严格类型检查的选项。

当你从简单的配置开始,并关闭严格的检查时,进入的门槛降低了,你的项目可以开始享受静态类型的好处。

TypeScript可以被引入到JavaScript文件的项目中,因为它输出的是JavaScript,在构建过程之后,所有东西都是JavaScript,这允许向TypeScript的更渐进的过渡,而不是将所有 *.js 文件重命名为 *.ts ,并可能得到许多错误(取决于配置设置),你可以从一个或两个TypeScript文件开始,最终,文件可以被重命名为 *.ts ,并且出现的新类型错误可以得到解决。

更好的第三方库集成
 

当使用第三方的 TypeScript 包时,你可能不会经常在编辑器和浏览器标签之间切换,因为 npm 上的许多库都包含 TypeScript 类型定义( d.ts )文件。

npm 网站还会给任何包含内置类型定义的包显示 TypeScript 徽章:

图片

社区提供的类型
 

还有很多包没有类型定义,为了支持这些情况,微软还运行了 DefinitelyTyped 项目,这是一个 GitHub 仓库,社区成员可以为缺少类型定义的库和工具提交类型定义。

这些类型在 @types 作用域下作为单独的包发布。这些类型通常不是由包的作者提供的。一般来说,除非一个包过时或被放弃,否则您很可能会找到您今天正在使用的任何包的类型定义。

如果你是一个库的作者,你甚至不需要手动编写定义,TypeScript 编译器可以配置为根据库中的模块自动生成这些  d.ts  文件。

缺点
 

TypeScript 也并非没有批评者。有时候,我实际上同意他们所说的。TypeScript 也有其缺陷,有时会令人恼火。

安全性未得到保证(运行时)
 

TypeScript很容易让人产生安全错觉,即使你整个项目都是用TypeScript写的,有严格的类型定义,并且开启了最严格的类型检查,你仍然不安全。

TypeScript 在构建时执行所有的类型检查,也许有一天浏览器会支持原生运行 TypeScript,但现在,TypeScript 编译器会检查你的代码,确保你没有任何类型错误,并输出可以在浏览器(或 Node.js 环境)中运行的纯 JavaScript。

这个生成的JavaScript不包含类型检查。没错,在运行时,它全部消失了。至少你可以确信,它处理的任何代码都不会有类型错误;你的应用不会因为有人试图在 Date 对象上调用 splice 而崩溃。

大多数应用程序并不是在真空中存在的,当你从 API 请求数据时会发生什么?假设你编写了一个函数来处理来自文档良好的 API 的数据,你可以创建一个接口来模拟预期的数据形状,所有与之一起工作的函数都使用这个类型信息。

也许这个特定的服务改变了他们的API数据格式,而你错过了更新,突然间你传递的数据与函数的类型定义不匹配,砰!

但这并不意味着你什么都失去了。Sneh Pandya 在 Methods for TypeScript runtime type checking 中讨论了一些在运行时检查类型的选项。

TypeScript 增加了额外的复杂性,即使是简单的任务
 

让我们使用 JavaScript 来监听文本输入框的输入事件:

document.querySelector('#username').addEventListener('input', event => {  console.log(event.target.value);});

无论用户何时输入,都会输出到控制台。这在浏览器中运行良好。

让我们把同样的代码放到 TypeScript 中,它会给我们一些关于  event.target.value  引用的错误。

第一个是:

Object is possibly 'null'.

TypeScript推断事件参数的类型是 Event ,这是所有DOM事件的基本接口,根据规范, Event 的目标属性可能是 null (例如,直接创建一个没有给定目标的 Event 对象)。

DOM 类型定义将  Event.target  的类型指定为  EventTarget | null ,而 TypeScript 告诉我们  event.target  可以是  null  —— 不是基于我们的代码,而是基于 DOM 类型定义本身。

在这个例子中,我们知道  event.target  会被定义,因为事件来自于  <input>  元素,我们为它添加了一个监听器,我们可以安全地假设  event.target  不是空的,为此,我们可以使用 TypeScript 的非空断言运算符( !  运算符),这个运算符应该非常小心使用,但在这里它是安全的:

input.addEventListener('input', event => {  console.log('Got value:', event.target!.value);});

这解决了“possibly null”错误,但还有另一个:

Property 'value' does not exist on type 'EventTarget'.

我们知道这里的event.target指向<input>元素(一个HTMLInputElement),但是类型定义说  event.target 是 EventTarget 类型,它没有 value 属性。

为了安全地访问 value 属性,我们需要将event.target强制转换为 HTMLInputElement :

input.addEventListener('input', event => {  const target = event.target as HTMLInputElement;  console.log('Got value:', target.value);});

注意,我们不再需要!操作符,当我们把event.target转换为 HTMLInputElement 时,我们没有考虑它成为 null 的可能性(如果有这种可能性,我们会把它转换为 HTMLInputElement | null )。

错误消息可能难以解读
 

简单的类型错误通常很容易理解和修复,但有时 TypeScript 会产生一些无用的错误消息,最糟糕的是无法理解。

下面是一个简单的用户数据库的代码:

interface User {  username: string;  roles: string[];}
const users: User[] = [  { username: 'bob', roles: ['admin', 'user']},  { username: 'joe', roles: ['user']}];

我们有一个用户列表和一个描述用户的类型定义,每个用户有一个用户名和一个或多个角色,下面写一个函数,生成这些用户的所有唯一角色的数组:

const roles = users.reduce((result, user) => { // Huge error here!  return [    ...result,    ...user.roles.filter(role => !result.includes(role)) // Minor error here  ];}, []);

我们从一个空数组开始,对于每个用户,我们添加我们还没有看到的每个角色。

我们得到的错误消息可能会吓跑 TypeScript 的初学者:

No overload matches this call.  Overload 1 of 3, '(callbackfn: (previousValue: User, currentValue: User, currentIndex: number, array: User[]) => User, initialValue: User): User', gave the following error.    Argument of type '(result: never[], user: User) => Role[]' is not assignable to parameter of type '(previousValue: User, currentValue: User, currentIndex: number, array: User[]) => User'.      Types of parameters 'result' and 'previousValue' are incompatible.        Type 'User' is missing the following properties from type 'never[]': length, pop, push, concat, and 26 more.  Overload 2 of 3, '(callbackfn: (previousValue: never[], currentValue: User, currentIndex: number, array: User[]) => never[], initialValue: never[]): never[]', gave the following error.    Argument of type '(result: never[], user: User) => Role[]' is not assignable to parameter of type '(previousValue: never[], currentValue: User, currentIndex: number, array: User[]) => never[]'.      Type 'Role[]' is not assignable to type 'never[]'.        Type 'string' is not assignable to type 'never'.          Type 'string' is not assignable to type 'never'.

我们如何解决这个巨大的错误消息?我们需要对传递给  reduce  的回调做一个小的更改。TypeScript 已经正确地推断出user参数的类型是 User (因为我们在一个 User 数组上调用 reduce ),但是它找不到  result  数组的类型。在这种情况下,TypeScript 给空数组赋予了 never[]  的类型。

为了修复这个问题,我们可以给 result 数组添加一个类型,并告诉 TypeScript 这是一个 Role 数组,这样错误就消失了,构建过程也成功了。

const roles = users.reduce((result: Role[], user) => {  return [    ...result,    ...user.roles.filter(role => !result.includes(role))  ];}, []);

我们收到的错误消息虽然技术上是正确的,但很难理解(特别是对于初学者而言)。

构建性能可能会受到影响
 

TypeScript 的类型检查好处并不是没有代价的,类型检查会减慢构建过程,特别是在大型项目中,如果你运行的开发服务器在代码更改时会重新加载,那么 TypeScript 构建步骤会因为等待代码构建而减慢开发速度。

有一些方法可以解决这个问题,例如,可以使用 Babel 而不是 TypeScript 编译器,通过@babel/plugin-transform-typescript plugin 将 TypeScript 转译为 JavaScript,这个插件只进行转译,不执行类型检查。

类型检查可以通过运行带有 noEmit 选项的 TypeScript 编译器来单独完成,这将检查类型,但不会输出任何 JavaScript,通过使用两步流程,当你需要一个快速开发服务器时,可以跳过类型检查,并可以作为额外的测试步骤或生产构建的一部分。

这不是万无一失的
 

当我们把某个东西强制转换成另一种类型或者使用转义,比如非空断言操作符时,TypeScript 相信我们的话。如果我在表达式后面添加 ! 操作符,TypeScript 不会警告我它可能是 null 。我可能错过了可能的场景,其中值实际上可能是 null 并引入了一个 bug.前面关于错误安全感的警告也适用于这里。

由于这有效地禁用了类型检查,许多项目使用了 ESLint 规则来禁止使用 ! 操作符。

好坏都接受
 

这些好处和痛点会对不同的团队产生不同的影响。对于一些人来说,好处可能超过了性能、严格性和无用的错误。然而,其他人可能不认为值得这样做。

当遇到一个令人困惑的错误时,一些开发人员可能会将值强制转换为 any ,这通常会满足 TypeScript 的要求(但代价是可能出现一些本可以避免的 bug)。

有效地使用 TypeScript 需要耐心和纪律,要倾听编译器告诉你什么,花时间正确地解决一个棘手的错误可能是痛苦的,但如果你坚持下去,通常会有回报。

另一方面,如果你打算使用 ! 操作符,将它转换为  any ,并使用其他技巧来消除错误,那么你将无法获得 TypeScript 的全部好处。

 欢迎关注公众号:文本魔术,了解更多

  • 27
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值