这个文章列举了我们建议尽量避免的 TypeScript 语法。但是因为你的项目的情况,有可能使用这些特性也是合理的,但是我们仍然建议,在默认情况下,尽量避免使用这些特性。
随着时间的发展,TypeScript 已经是一门复杂的语言。在早期的时候,TypeScript 研发团队增加了一些不兼容 JavaScript 的语法。但是随着发展,新的版本已经不会这么做了,会非常保守地和严格地遵循 JavaScript 的语法特性。(译者注,使用和 JavaScript 严格兼容的语法带来的好处不计其数。)
就像其他成熟的语言,我们在考虑 TypeScript 的语法使用哪些,避免哪些,并不是一个容易的决定。我们经验主要来自于
Execute Program
[1]
的后端和前端的建设经验,以及创建我们的
TypeScript 课程
[2]
时的经验。
避免枚举(课程
[3]
)
枚举提供了一组常量。在下面的例子里,HttpMethod.Get 是字符串 ‘Get’ 的名字。HttpMethod 类型和一个联合类型是一样的,如
'GET' | 'POST'
。
enum HttpMethod {
Get = 'GET',
Post = 'POST',
}
const method: HttpMethod = HttpMethod.Post;
method; // Evaluates to 'POST'
下面是支持使用枚举的原因:
假设,我们最终要替换 ‘POST’ 为 ‘post’。我们只要替换枚举的值就能达成这一目的。我们其他的代码因为引用的是
HttpMethod.Post
[4]
,所以完全不用改。
现在假设,如果我们用联合类型来实现这个场景。我们定义了联合类型 'GET' | 'POST',然后我们决定把它们改为小写的 'get' | 'post'。现在如果使用 'GET' 或者 'POST' 作为 HttpMethod 的代码就会报类型错误。我们需要把所有的代码手动改一遍。从这个例子来说,使用枚举能简单一些。
这个支持使用枚举的例子可能不是那么有说服力。当我们增加了一个枚举和联合类型的时候,实际上在创建以后是很少更改的。使用联合类型,确实会带来更多的更改成本,但是其实不是一个问题,因为实际上是很少更改的。即便要更改,因为有类型错误,我们并不害怕少改了。
使用枚举的坏处是:
我们需要适应 TypeScript 的语法。TypeScript 应该是 JavaScript,但是增加了静态类型。如果我们去掉 TypeScript 的类型,我们就应该得到一份完整有效的 JavaScript 的代码。(译者注:这个原因是整篇文章的核心,核心好处之一就是,你可以通过 esbuild 而不是 tsc 完成你的 ts 代码到 js 代码的转换,这个速度差距可能是 10-1000倍。。并且不引入 tsc,代表着少了一个可能出问题的地方。)在 TypeScript 的官方文档中,之前描述 TypeScript 的文档是 “类型级别的扩展”:即 TypeScript 是 JavaScript 类型级别的扩展,所有 TypeScript 的特性不改变运行时的行为。
下面是一个类型级别扩展的例子, TypeScript 的例子:
function add(x: number, y: number): number {
return x + y;
}
add(1, 2); // Evaluates to 3
TypeScript 的编译器检查了代码的类型。然后生成了 JavaScript 的代码。很幸运,这个过程很简单:编译器只要把类型标注去掉就好了。在这个例子里,只要把 :number 去掉,下面就是完美的 JavaScript 代码:
function add(x, y) {
return x + y;
}
add(1, 2); // Evaluates to 3
绝大部分 TypeScript 的特性都有这个特性,遵循了类型级别扩展的法则。要得到 JavaScript 代码,只需要去掉类型标准即可以。
然而,枚举打破了这个法则。HttpMethod 和
HttpMethod.Post
[5]
是一部分的类型。他们应该被去除。然而,如果编译器去除这些代码,就会有问题,因为我们实际上在把
HttpMethod.Post
[6]
当成值类型在使用。如果编译器简单删除这些代码,这些代码就不能跑了。
/* This is compiled JavaScript code referencing a TypeScript enum. But if the
* TypeScript compiler simply removes the enum, then there's nothing to
* reference!
*
* This code fails at runtime:
* Uncaught ReferenceError: HttpMethod is not defined */
const method = HttpMethod.Post;
TypeScript 的解决方案,就是打破自己的规则。当编译一个枚举的时候,编译器会自己生成一些 JavaScript 代码。其实很少 TypeScript 特性会这样做,这个其实让 TypeScript 的编译模型变得复杂了。因为这些原因,我们建议避免使用枚举,而用联合类型来取代它。
为什么类型级别扩展这个规则这么重要呢?
让我们来看,这个法则在和 JavaScript 和 TypeScript 的工具链生态互动时,会发生什么。TypeScript 的项目都是从 JavaScript 项目继承而来的,所以使用打包工具和编译工具,例如 webpack 和 babel 是很正常的。这些工具都是为了 JavaScript 设计的,即便在今天,依然是关注在 JavaScript 上。每一个工具都有自己的生态。这里有无数的 Babel 和 Webpack 自有的生态的插件。
有可能让所有 Babel 和 Webpack 以及他们的生态插件支持 TypeScript 么?对于大部分 TypeScript 语言来说,实际上类型扩展规则让这些内容支持 TypeScript 很简单。工具只要去掉类型标准,然后对其余的 JavaScript 做剩下的工具就好了。
当对于像枚举这样的特性(包括名字空间 namespaces),这个事儿要复杂一些。不能简单移除枚举。工具需要把 enum HttpMethod { ... } 转译 成合适的 JavaScript 代码,因为 JavaScript 并没有 enum 关键字。
这会带来一些实际的工作量,来处理 TypeScript 自己打破自己的类型扩展法则的问题。像 Babel、webpack 以及他们的生态插件,都是先对 JavaScript 作为设计对象,TypeScript 一般来说只是他们支持的一个功能。很多时候,TypeScript 的支持并不能收到像 JavaScript 一样的支持,就会有很多 Bug。(译者注:考虑 JavaScript 实际上让这些工具和插件的难度小很多,考虑 TypeScript,很多问题其实变复杂了,而且这个复杂度的提升不一定是有价值的。时至今日,依然是 JavaScript 的代码和需求远远大于 TypeScript。即便出于降低这些工具的复杂度的目的,也不应该为了解决 TypeScript 的问题而引入 这些问题。最核心的运行时,依然,以及必然是 JavaScript。)
很多工具的工作主要是在处理变量声明和函数声明,这些事情其实相对都是比较容易做的。但是牵扯枚举和名字空间,就不能仅仅去掉类型标注开始做逻辑了。你当然可以信赖 TypeScript 的编译器,但是很多不常用的工具可不一定考虑这个问题了。
当你的编译器、打包器、压缩器、linter、代码格式器(译者注:其实代码格式器很容易造成 bug,尤其对于 TypeScript)只要发生了一个对于上述的事儿处理有问题,是非常难进行 debug 的。编译器的 Bug 是
非常非常难找