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 的全部好处。
欢迎关注公众号:文本魔术,了解更多