本文章列举了四个应该避免使用的Typescript特性。也许在你的开发过程中,你有理由去使用它们,但我们认为默认应该避免使用它们。
TypeScript是一门逐渐复杂的语言。在早期开发过程中,团队基于js增加了一些兼容性的特性。最近的开发更加保守,更严格的保证对js特性的兼容。
像其他成熟的编程语言一样,决定哪些特性应该避免使用对我们来说是很艰难的。我们体验的了这些优缺点在开发Execute Program和的前端和后端的过程中。基于我们的经验,以下四个特性应该避免使用。
1. Enums
Enums 用于常量的枚举。在下面的例子里 HttpMethod.Get 是字符串'GET'的命名,HttpMethod类型是两个字符类型的集合。
enum HttpMethod {
Get = 'GET',
Post = 'POST',
}
const method: HttpMethod = HttpMethod.Post;
method; // Evaluates to 'POST'
首先是enum的优点:
假如我们需要把代码中的 'POST'都替换成小写的'post',我们只需要吧枚举定义里的值换成'post'就好了。其他代码都是依据这个定义,所以无需改动其他。
想象同样的场景使用集合类型替代enum,'GET' | 'POST'。当我们要修改成'post'时,所有使用了这个类型的地方都会报错,我们必须手动把所有报错的地方修正。相比于enum,这是集合类型额外要做的。
但是这个优点其实挺小的。当我们创建了一个enum或union,很少会去改动它。实话实说,用union是要花点时间去更新其他代码,但问题并不大,因为是个非常低频的操作。就算发生了,类型检测会保证代码的正确性。
enums的缺点来自它们对js的补充。ts应该只是一门对js的进行类型扩充的语言。如果我们移除所有ts类型,应该只剩下可以直接运行的js。大多数ts特性都是这样,它们不会影响js的运行行为。
这里是一个类型扩充的例子:
function add(x: number, y: number): number {
return x + y;
}
add(1, 2); // Evaluates to 3
编译器检查完类型后,需要生成js代码。过程很简单:编译器把所有类型声明去掉即可。在这个例子里去掉: number,就剩下全是完美合法的js代码。
function add(x, y) {
return x + y;
}
add(1, 2); // Evaluates to 3
大多数ts特性都是这样的,遵从类型扩充的规则。只要移除类型就能得到js代码。
但是enums打破了这个规则。HttpMethod 和 HttpMethod.Post 是一种类型,所以在编译js的时候如果也直接移除类型声明的话,代码其他地方仍依赖HttpMethod.Post,这时候代码就会报错了。
/*
* 运行时报错:
* Uncaught ReferenceError: HttpMethod is not defined */
const method = HttpMethod.Post;
ts在这个场景的解决方案打破了自己的规则。编译enum的时候,编译器会添加额外的js代码,这些是你原来代码里不存在的。很少有ts特性会像这样,但这些特性无疑增加了令人困惑的编译逻辑。基于这个原因,我们推荐使用union替换enum。
为什么类型拓展这么重要?
让我们理下js和ts的转换工具。ts项目根本来说就是js项目,所以我们经常使用js构建工具像babel和webpack,这些工具和插件完全支持了ts吗?大多数ts的特性让这些工具的任务很简单,但遇到类型enum这样的特性(包括namespace,下面会提到)就开始变得复杂了,它们开始入侵js代码,因为js根本没有enum。
ts违背了类型扩充的规则,它带来了真实的困难。Babel,webpack之类工具,本职是js编译,ts编译只是众多功能之一,它没有像js那么多的关注,容易产生bug。特别是一些更小众的工具,就更容易出问题。
当你的编译器,打包工具,压缩工具,lint工具,格式纠正工具一起对你的代码进行修改的时候,debug开始变得困难。编译器bug是很难定位的notoriously difficult。你往往会看到的是经过几个礼拜的排查,我们了解到导致bug的原因。
2. namespace
Namespaces和modules一样,是希望在一个文件里能有多个命名空间。举个例子,我们希望一个文件里定义不同的namespace,然后到处来进行测试(我们不推荐这种做法,这里只是为了举例)。
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();
Namespaces在使用中会有问题。在上面enum部分里,我们说明了类型拓展对于ts的意义,Namespaces一样打破了这个规则。namespace Util { export function wordCount ... }这个定义我们无法移除类型的定义。整个namespace就是类型定义,如果移除掉,那其他调用Util.wordCount(...)的地方就无法工作了。
enum我们推荐使用union,namespace我们推荐使用普通的module。可能要多建一些文件,但module就是有namespace一样的功能却避免了潜在的问题。
3. decorators(至少现在)
装饰器是对函数或类的修改或替代,下面是一个官方装饰器例子:
// This is the decorator.
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
ts是更早于js把装饰器加入语言特性的。在2022年一月,js装饰器仍处于stage2就是草稿“draft”状态的,我们推荐使用js装饰器直到它处于只是stage3(备选)状态时。
js装饰器是有可能不成为es标准的。如果真的发生了,它们就会像enum和namespace一样。我们只能依赖第三方的工具编译这种特性。所以装饰器的优点还不值得我们冒这个险。
有的开源的仓库,最出名的TypeORM,重度使用了装饰器。不使用装饰器和使用typeorm并不冲突。typeorm是个很好的选择,但也应该记得我们上面提到的,装饰器仍然有可能不被采纳入es标准。
4. private
ts有两个方式创建私有变量。一个是ts的private语法,一个是js的新语法#somePrivateField,下面就是例子。
class MyClass {
private field1: string;
#field2: string;
...
}
我们推荐用#somePrivateField语法,因为:两个特性其实很接近,但我们更偏向于使用js原生的特性。
总结
虽然我们不推荐这些特性,但是了解它们运行原理也是有益无害的。当然也有人是不同意的欢迎以在评论区留下你的意见。