TypeScript 开发中应谨慎使用的四类语法特性解析
本文总结了 TypeScript 开发中建议避免的四种语法:枚举、命名空间、装饰器和 private 关键字。核心原因在于这些特性打破了 TypeScript 作为 JavaScript 类型级扩展的原则,导致编译后的 JavaScript 代码行为改变,进而引发工具链兼容性问题及构建复杂度增加。文章提供了联合类型、ESM 模块和 # 私有字段等更优替代方案,旨在帮助开发者提升代码可维护性与构建效率。
前言
本文梳理了建议在实际项目中谨慎使用的 TypeScript 语法特性。尽管特定项目场景下使用这些特性可能具有合理性,但在默认情况下,我们仍强烈建议开发者尽量避免采用这些写法。
随着 TypeScript 语言的演进,其复杂性日益增加。早期版本中曾引入过一些与 JavaScript 不兼容的语法结构,但后续版本已大幅收敛,转而采取保守策略,严格遵循 JavaScript 的语法规范。这种与 JavaScript 保持严格兼容的做法,能带来诸多显著优势。
如同其他成熟编程语言一样,在 TypeScript 中权衡“使用”与“避免”某些语法并非易事。我们的建议主要基于 Execute Program 在后端与前端构建中的实践经验,以及开发 TypeScript 课程时的总结。
避免枚举
枚举(Enum)用于定义一组常量。例如,在下面的代码中,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' 的代码将报类型错误,需手动逐一修改。从这一角度看,枚举似乎更为简便。
然而,上述支持枚举的理由说服力有限。在实际开发中,枚举和联合类型一旦定义,后续修改频率极低。虽然使用联合类型确实会增加修改成本,但由于修改场景罕见,这通常不构成问题。即便需要修改,类型错误也会提示开发者,无需担心遗漏。
使用枚举的主要弊端在于:
开发者需适应 TypeScript 特有的语法。TypeScript 本质上是 JavaScript 加上静态类型。理想状态下,移除 TypeScript 的类型标注后,应得到一份完整且有效的 JavaScript 代码。这也是本文的核心观点之一:使用如 esbuild 等工具而非 tsc 进行转换,速度可提升 10 至 1000 倍,且减少了引入 tsc 可能带来的潜在问题。官方文档曾描述 TypeScript 为“类型级别的扩展”,即所有 TypeScript 特性不应改变运行时行为。
以下是一个类型级扩展的示例:
function add(x: number, y: number): number {
return x + y;
}
add(1, 2); // Evaluates to 3TypeScript 编译器负责检查代码类型,并生成 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 编译器,但许多不常用的工具未必考虑此问题。
当编译器、打包器、压缩器、linter 或代码格式器中任一工具对上述特性处理存在缺陷时,Debug 将极其困难。编译器 Bug 极难定位(译者注:若不使用这些特性,代码将不依赖 TypeScript 编译器,这至关重要)。本文内容基于数周研究及同事协助,深入明确了 Bug 涉及范围(注意加粗部分)。(译者注:我曾耗时约两个月研究 TypeScript 装饰器及元数据并计划引入自研框架,最终因 esbuild 不支持 TS 装饰器元数据(尽管支持较新的装饰器)且框架以 esbuild 为基础而沮丧放弃)。(译者注:引入 tsc 不明智,因 tsc 过于复杂;若仅使用类型,编写阶段已基本完成 tsc 工作,最后用 esbuild 去除类型即可。)
避免命名空间
命名空间(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();命名空间在实践中会引发问题。在前述枚举示例中,我们看到了 TypeScript 的类型扩展法则:通常,移除类型标注后留下的即为 JavaScript 代码。
命名空间同样打破了这一设定。在 namespace Util { export function wordCount ... } 代码中,仅移除类型标注无法获得 JavaScript 代码。整个命名空间即为 TypeScript 的类型定义!若在其它代码中使用 Util.wordCount(...),删除 Util 命名空间并生成 JavaScript 代码后,Util 将不复存在,Util.wordCount(...) 也无法工作。
与枚举类似,TypeScript 无法仅删除命名空间定义,而必须生成相应的 JavaScript 代码。
对于枚举,我们建议使用联合类型替代;对于命名空间,建议使用 ESM(ECMAScript 模块)替代。虽然创建多个文件较为繁琐,但两者达成的效果完全一致。
避免装饰器(现阶段)
装饰器(Decorator)是用于修改或替换其他函数或类的方法。以下示例来自 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 阶段(Stage 2),即“草案”(Draft)阶段。该提案似乎长期停滞在委员会中:实际上,该提案于 2019 年 2 月进入第 2 阶段。
我们建议在进入第 3 阶段(Candidate,候选阶段)或第 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 避免使用的语法内容,请搜索本站以往文章或继续浏览后续相关文章。感谢大家的支持!
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38








