关于TypeScript应该尽量避免的语法总结
目录
- 前言
- 避免枚举
- 避免名字空间
- 避免装饰器(对于现在而言)
- 避免 Private 关键字
- 总结
前言
这个文章列举了我们建议尽量避免的 TypeScript 语法。但是因为你的项目的情况,有可能使用这些特性也是合理的,但是我们仍然建议,在默认情况下,尽量避免使用这些特性。
随着时间的发展,TypeScript 已经是一门复杂的语言。在早期的时候,TypeScript 研发团队增加了一些不兼容 JavaScript 的语法。但是随着发展,新的版本已经不会这么做了,会非常保守地和严格地遵循 JavaScript 的语法特性。(译者注,使用和 JavaScript 严格兼容的语法带来的好处不计其数。)
就像其他成熟的语言,我们在考虑 TypeScript 的语法使用哪些,避免哪些,并不是一个容易的决定。我们经验主要来自于Execute Program 的后端和前端的建设经验,以及创建我们的 TypeScript 课程时的经验。
避免枚举
枚举提供了一组常量。在下面的例子里,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 ,所以完全不用改。
现在假设,如果我们用联合类型来实现这个场景。我们定义了联合类型 '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是一部分的类型。他们应该被去除。然而,如果编译器去除这些代码,就会有问题,因为我们实际上在把 HttpMethod.Post 当成值类型在使用。如果编译器简单删除这些代码,这些代码就不能跑了。
/* 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 是非常非常难找的(译者注:当出现一个 bug,你会第几直觉认为是编译器的错误呢?其实不使用这些特性,你的代码是不依赖 TypeScript 编译器的,这一点至关重要。)。主要这篇文章的这些文字:经历了几周以后,在我的同事的帮助下,我们对于这个bug牵扯的范畴有了更深的认识。(注意加粗字体)(译者注:我本来花了大约两个月的时间去研究 TypeScript 的装饰器以及装饰器元数据,然后计划把他们加入到我自己的框架里。但是最后沮丧的发现,如果我引入他们,我就没有办法用 esbuild 了,原因是 esbuild 不计划支持 TypeScript 的装饰器元数据,但是支持了装饰器,但是这个支持其实也很新,而我的整个框架其实是以 esbuild 为基石的。我很沮丧,放弃了 TypeScript 的装饰器)(译者注:引入 tsc 是不明智的,因为 tsc 非常非常复杂。实际上,你只用类型的话,在代码编写阶段基本也就完成了绝大部分 tsc 的事情。在最后用 esbuild 一去类型,就可以继续了。)
避免名字空间
名字空间类似 module,但是一个文件里可以有多个名字空间。例如,我们在一个文件里引入了不同名字空间的导出代码,以及它们对应的测试。(我们不建议这样使用名字空间,这里只是作为一个探讨的例子。)
namespace Util { export function wordCount(s: string) { return s.split(/\b\w+\b/g).length - 1; } } namespace Tests { export function testWordCount() { if (Util.wordCount('hello there') !== 2) { throw new Error("Expected word count for 'hello there' to be 2"); } } } Tests.testWordCount();
名字空间在实践上会造成一些问题。在上面的枚举的例子里,我们看到了 TypeScript 的类型扩展法则。通常,TypeScript 去除类型标注,留下的就是 JavaScript 的代码。
名字空间自然也打破了这一设定。在 namespace Util { export function wordCount ... } 代码里,我们不能仅仅靠去除类型标注就获得 JavaScript 的代码。整个名字空间就是一个 TypeScript 的类型定义!在其他的代码里使用 Util.wordCount(...) 会发生什么呢?如果我们删除 Util 名字空间,然后生成 JavaScript 代码,Util 就没有了。所以 Util.wordCount(...) 也不能工作。
就和枚举一样,TypeScript 也不能仅仅删除名字空间定义,而要生成一些 JavaScript 的代码。
对于枚举,我们建议用联合类型来取代。对于名字空间,我们建议就用 ESM 取代就好了。虽然创建很多文件很麻烦。但是两者能达成的效果是完全一样的。
避免装饰器(对于现在而言)
装饰器是一个可以修改和取代其他函数或者类的方法。这里是一个从 TypeScript 官方文档里找到的装饰器的例子:
// This is the decorator. @sealed class BugReport { type = "report"; title: string; constructor(t: string) { this.title = t; } }
@sealed 装饰器暗示了C# 的 sealed 装饰器。这个装饰器可以防止其他类继承这个类。我们可以实现一个 sealed 的函数,然后接受一个类,修改它,让它不能继承这个类。
装饰器一开始是首先在 TypeScript 添加的,然后 JavaScript(ECMAScript)才开始了标准化进程。在 2022 年1月,装饰器依然是一个 ECMAScript 提案阶段 2 的提案。阶段 2 代表在 “draft”(起草)阶段。装饰器提案似乎一直停滞在委员会中:实际上,这个提案是在 2019 年 2 月到达阶段 2 的。
我们建议在 stage 3 前,避免使用装饰器。stage 3 指 “candidate” 阶段,或者 stage 4 “finished” 阶段。
有这样的可能,即 ECMAScript 永远不完成装饰器提案。如果这个提案不完成,装饰器的处境就和枚举、名字空间一样。使用装饰器,就代表着,打破了 TypeScript 的类型扩展规则,并且使用这个特性,很多打包工具,可能都是有问题的。我们不知道多会能让装饰器通过,但是装饰器带来的好处并没有那么大,所以我们选择等待。
一些开源的库,例如有名的 TypeORM,非常重的使用了装饰器。我们承认,如果遵守我们的建议,就不能使用 TypeORM。当然使用 TypeORM 和装饰器有时候是好的选择,但是你应该明白这么做带来的问题,你要知道,目前装饰器的提案的标准化过程可能永远不会结束。(译者注:如果你想享受 esbuild 带来的好处,装饰器用的深就可能是个问题。当然,如果你的业务可以自闭在一套装饰器写的框架里,可能也不是非常大的问题。但是,如果 JS 的装饰器出现,现有的装饰器框架可能就有问题了。)
避免 Private 关键字
TypeScript 有两种方式让一个类型属性私有。老的方法是 private 关键字,这个是 TypeScript 独有的。目前还有一个新的方式:#somePrivateField,这个是 JavaScript 的方式。下面是一个例子:
class MyClass { private field1: string; #field2: string; ... }
我们建议 #somePrivateField 字段。但是这两个方法基本是等同的。但是我们建议更多使用 JavaScript 的特性。
来总结一些我们的四个建议:
- 避免 enum 枚举
- 避免名字空间 namespace
- 避免装饰器,尽量等到这个语法标准化完成。如果你需要一个库用装饰器,要考虑它的标准化状态。
- 尽量用 #somePrivateField而不是private somePrivateField.
尽管我们建议大家避免使用这些特性,但是学习这些特性的知识还是大有裨益的。因为在遗产代码里,你还是会大量看到这些东西,甚至一些新的代码。我们相信,肯定不是所有人都认同这些建议。
总结
到此这篇关于TypeScript应该尽量避免的语法的文章就介绍到这了,更多相关TS尽量避免的语法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!