SoFunction
Updated on 2025-04-03

A summary of syntax that TypeScript should try to avoid

Preface

This article lists the TypeScript syntax we recommend to avoid as much as possible. However, it is reasonable to use these features because of your project, but we still recommend that by default, try to avoid using these features.

As time goes by, TypeScript has become a complex language. In the early days, the TypeScript R&D team added some syntax that was incompatible with JavaScript. But as it develops, new versions will no longer do this and will follow the syntax features of JavaScript very conservatively and strictly. (Translator's note, there are countless benefits to using a syntax that is strictly compatible with JavaScript.)

Just like other mature languages, it is not an easy decision to consider what TypeScript syntax uses and what to avoid. Our experience mainly comes from the experience of building the backend and frontend of the Execute Program, as well as the experience of creating our TypeScript courses.

Avoid enumeration

The enum provides a set of constants. In the following example, is the name of the string ‘Get’. The HttpMethod type is the same as a union type, such as 'GET' | 'POST'.

enum HttpMethod {
  Get = 'GET',
  Post = 'POST',
}
const method: HttpMethod = ;
method; // Evaluates to 'POST'

Here are the reasons why enumeration is supported:

Suppose, we end up replacing ‘POST’ with ‘post’. We can achieve this by replacing the enum value. Since our other code is referenced, we don't need to change it at all.

Now suppose, if we use union types to implement this scenario. We defined the union types 'GET' | 'POST' and then we decided to change them to lowercase 'get' | 'post'. Now if you use 'GET' or 'POST' as the HttpMethod code, you will report a type error. We need to manually change all the code. From this example, using enums can be easier.

This example of supporting the use of enumerations may not be so convincing. When we add an enum and union type, it is actually rarely changed after creation. Using a union type does bring more change costs, but it is not a problem, because it is actually very rarely changed. Even if we have to change it, because there are type errors, we are not afraid to change it less.

The disadvantages of using enums are:

We need to adapt to TypeScript's syntax. TypeScript should be JavaScript, but adds static types. If we remove the TypeScript type, we should get a complete and valid JavaScript code. (Translator's note: This is the core of the entire article. One of the core benefits is that you can complete the conversion of your ts code to js code through esbuild instead of tsc. This speed gap may be 10-1000 times. And the failure to introduce tsc means that there is a missing place where there may be problems.) In the official documentation of TypeScript, the document that described TypeScript before was "type-level extension": that is, TypeScript is a JavaScript type-level extension, and all the features of TypeScript do not change the behavior of the runtime.

Here is an example of type-level extension, an example of TypeScript:

function add(x: number, y: number): number {
  return x + y;
}
add(1, 2); // Evaluates to 3

TypeScript's compiler checks the type of code. Then the JavaScript code is generated. Fortunately, this process is very simple: the compiler just needs to remove the type annotation. In this example, just remove :number, and the following is the perfect JavaScript code:

function add(x, y) {
  return x + y;
}
add(1, 2); // Evaluates to 3

Most TypeScript features have this feature, following the rules of type-level extension. To get JavaScript code, you only need to remove the type standard.

However, enumeration breaks this rule. HttpMethod and are part of the type. They should be removed. However, if the compiler removes this code, there will be problems because we are actually using it as a value type. If the compiler simply deletes these codes, the code can't run.

/* 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 = ;

The solution to TypeScript is to break your own rules. When compiling an enum, the compiler will generate some JavaScript code by itself. In fact, few TypeScript features do this, which actually complicates the compilation model of TypeScript. For these reasons, we recommend avoiding enumerations and replacing it with union types.

Why is the type-level extension rule so important?

Let's see what happens when this rule interacts with the toolchain ecosystem of JavaScript and TypeScript. TypeScript projects are inherited from JavaScript projects, so it is normal to use packaging tools and compilation tools such as webpack and babel. These tools are designed for JavaScript, and even today, they are still focused on JavaScript. Each tool has its own ecosystem. There are countless plug-ins for Babel and Webpack's own ecological ones here.

Is it possible to enable TypeScript to support all Babel and Webpack and their eco plugins? For most TypeScript languages, it is actually easy to make these contents support TypeScript. The tool just removes the type standard and then makes the remaining tools for the rest of JavaScript.

This is a bit more complicated when it comes to features like enums (including namespaces). Enumeration cannot be simply removed. The tool needs to translate enum HttpMethod { ... } into appropriate JavaScript code, because JavaScript does not have the enum keyword.

This will bring some practical workload to deal with TypeScript's own breaking of its own type extension law. Like Babel, webpack and their ecological plug-ins, JavaScript is designed first, and TypeScript is generally just a feature they support. Many times, TypeScript support cannot receive support like JavaScript, and there will be many bugs. (Translator's note: Considering JavaScript actually makes these tools and plug-ins much less difficult. Considering TypeScript, many problems actually become complicated, and this complexity increase is not necessarily valuable. To this day, JavaScript's code and requirements are still much greater than TypeScript. Even for the purpose of reducing the complexity of these tools, these problems should not be introduced to solve the TypeScript problem. The most core runtime is still, and must be JavaScript.)

Many tools mainly work on variable declarations and function declarations, which are relatively easy to do. However, if you involve enumeration and namespace, you cannot just remove the type annotation and start to make logic. Of course you can trust TypeScript's compiler, but many uncommonly used tools may not necessarily consider this issue.

When your compiler, packer, compressor, linter, code formatter (Translator's note: In fact, code formatters are easy to cause bugs, especially for TypeScript), as long as there is a problem with the above-mentioned things, it is very difficult to debug. Compiler bugs are very, very difficult to find (Translator's note: When a bug occurs, which intuition do you think is a compiler error? In fact, if you don't use these features, your code does not rely on the TypeScript compiler, which is crucial.). Mainly, these words in this article: After a few weeks, with the help of my colleagues, we have a deeper understanding of the scope of this bug. (Note the bold font) (Translator's note: I spent about two months studying the decorators and decorator metadata of TypeScript, and then planned to add them to my own framework. But in the end I was frustrated that if I introduced them, I wouldn't be able to use esbuild because esbuild does not plan to support the decorator metadata of TypeScript, but it supports decorators, but this support is actually very new, and my entire framework is actually based on esbuild. I was frustrated and gave up the decorator of TypeScript) (Translator's note: It's unwise to introduce tsc, because tsc is very, very complicated. In fact, if you only use types, you will basically complete most of the tsc things in the code writing stage. At the end, you can continue with the type using esbuild.)

Avoid namespace

The name space is similar to the module, but there can be multiple name spaces in a file. For example, we introduce export codes with different namespaces in a file, as well as their corresponding tests. (We do not recommend using namespaces like this, here is just an example of discussion.)

namespace Util {
  export function wordCount(s: string) {
    return (/\b\w+\b/g).length - 1;
  }
}

namespace Tests {
  export function testWordCount() {
    if (('hello there') !== 2) {
      throw new Error("Expected word count for 'hello there' to be 2");
    }
  }
}

();

Namespaces can cause some problems in practice. In the example enumeration above, we see the type extension rule of TypeScript. Usually, TypeScript removes type annotations, leaving behind JavaScript code.

The name space naturally breaks this setting. In the namespace Util { export function wordCount ... } code, we cannot obtain JavaScript code just by removing type annotations. The entire namespace is a TypeScript type definition! What happens when using (...) in other codes? If we delete the Util namespace and generate JavaScript code, Util will be gone. So (...) doesn't work either.

Just like enumerations, TypeScript cannot just delete the namespace definition, but generates some JavaScript code.

For enumerations, we recommend replacing them with union types. For namespaces, we recommend using ESM instead. Although creating a lot of files is cumbersome. But the effect that both can achieve is exactly the same.

Avoid decorators (for now)

A decorator is a method that can modify and replace other functions or classes. Here is an example of a decorator found in the official TypeScript documentation:

// This is the decorator.
@sealed
class BugReport {
  type = "report";
  title: string;

  constructor(t: string) {
     = t;
  }
}

The @sealed decorator hints at the sealed decorator for C#. This decorator prevents other classes from inheriting this class. We can implement a sealed function, then accept a class, modify it so that it cannot inherit the class.

The decorator is first added in TypeScript, and then JavaScript (ECMAScript) begins the standardization process. In January 2022, the Decorator remains a proposal for ECMAScript proposal Phase 2. Phase 2 represents the “draft” (draft) phase. The decorator proposal seems to have been stuck on the committee: In fact, this proposal arrived in Phase 2 in February 2019.

We recommend avoiding decorators before stage 3. stage 3 refers to the “candidate” stage, or the “finished” stage.

There is a possibility that ECMAScript will never complete the decorator proposal. If this proposal is not completed, the decorator's situation will be the same as the enumeration and namespace. Using a decorator means breaking the type extension rules of TypeScript, and using this feature, many packaging tools may have problems. We don't know how much we can get the decorator through, but the benefits of the decorator are not that great, so we chose to wait.

Some open source libraries, such as the famous TypeORM, use decorators very heavily. We acknowledge that TypeORM cannot be used if we follow our advice. Of course, using TypeORM and decorators is sometimes a good choice, but you should understand the problems that come with doing so. You should know that the current standardization process of decorators' proposals may never end. (Translator's note: If you want to enjoy the benefits of esbuild, the use of decorators can be a problem. Of course, if your business can be self-contained in a frame written by decorators, it may not be a big problem. However, if JS decorators appear, existing decorators may have problems.)

Avoid Private keywords

TypeScript has two ways to make a type attribute private. The old method is the private keyword, which is unique to TypeScript. There is also a new way: #somePrivateField, which is the JavaScript way. Here is an example:

class MyClass {
  private field1: string;
  #field2: string;
  ...
}

We recommend the #somePrivateField field. But these two methods are basically the same. But we recommend more features using JavaScript.

Let’s summarize some of our four suggestions:

  • Avoid enum enums
  • Avoid namespace
  • Avoid decorators and try to wait until this syntax is standardized. If you need a library decorator, consider its standardized state.
  • Try to use #somePrivateField instead of private somePrivateField.

Although we recommend that you avoid these features, it is of great benefit to learn the knowledge of these features. Because in legacy code, you will still see these things in large quantities, even some new code. We believe that certainly not everyone agrees with these suggestions.

Summarize

This is the article about the syntax that TypeScript should try to avoid. For more information about TS, please search for my previous articles or continue browsing the related articles below. I hope you will support me in the future!