The body of the
This article lists four TypeScript features you are advised to avoid. Depending on how you use them, you might have a good reason to use these features, but we don’t think they should be used by default.
TypeScript has evolved over time into a complex language. Early in its iteration, the TypeScript team added features that were incompatible with JavaScript. Recent iterations have been more conservative, maintaining tighter compatibility with JavaScript features.
As with any mature language, we have to make hard choices about which TypeScript features to use and which to avoid. Our team experienced these trade-offs firsthand when building backend and front-end projects for Execute Program with TypeScript, and when creating our comprehensive TypeScript curriculum. Based on our experience, here are four suggestions for which features to avoid.
1. Avoid enum
Enum names a set of constants. In the following example, httpmethod. Get is a name for the string ‘Get’. HttpMethod type is conceptually similar to the type of joint between literal type, such as’ GET ‘|’ POST ‘.
enum HttpMethod {
Get = 'GET',
Post = 'POST',}const method: HttpMethod = HttpMethod.Post;
method; // Evaluates to 'POST'
Copy the code
Here are the arguments in favor of enum:
Suppose we need to replace the above string ‘post’ with ‘post’ one day later. We just change the enumeration to ‘post’! Other code in the system still references the enumerator via httpMethod.post, and the enumerator still exists.
Now imagine the same scenario, with a union type instead of an enumeration. We define the type of joint type HttpMethod = ‘GET’ | ‘POST’ and later decided to be changed to ‘GET’ | ‘POST’. Any code that tries to use ‘GET’ or ‘POST’ as HttpMethod will now have a type error. We have to update this code manually, which is an extra step compared to enumerations.
This code maintenance argument for enUms is not strong. When we add a new member to an enumeration or union type, we rarely modify it after it has been created. If we use federated types, it is true that we might spend some time updating the relevant code in multiple places, but this is not a big problem because it rarely happens. Even if it does, the type error tells us what code updates should be made.
The downside of enums is how they fit into the TypeScript language. TypeScript is JavaScript with static typing. If we remove all type code from TypeScript code, what remains should be valid JavaScript code. The official term used in TypeScript documentation is “type-level extension” : Most TypeScript features are type-level extensions to JavaScript, and they don’t affect the runtime behavior of your code.
Here is a concrete example of TypeScript extension. We write this TypeScript code:
function add(x: number, y: number) :number {
return x + y;
}
add(1.2); // Evaluates to 3
Copy the code
The compiler checks the type of code. It then generates JavaScript code. Fortunately, this step is easy, and the compiler just needs to remove all the type annotations. In this case, that means deleting: number. All that’s left is perfectly legal JavaScript code.
function add(x, y) {
return x + y;
}
add(1.2); // Evaluates to 3
Copy the code
Most TypeScript features work this way, following type-level extension rules. To generate JavaScript code, the compiler only needs to remove the type comments.
Unfortunately, enumerations break this rule. HttpMethod and httpmethod.post are part of the same type, so they should be removed when TypeScript generates JavaScript code. However, if the compiler simply removed the enumerated types from our code example above, we would still be left with JavaScript code that references httpMethod.post. This can go wrong during program execution: if the compiler removes httpMethod.post, we can’t reference it!
/* 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;
Copy the code
In this case, TypeScript’s solution is to break its own rules. When compiling an enumeration, the compiler emits additional JavaScript code that is not present in the original TypeScript code. Few TypeScript features work like enumerations, and each feature adds a puzzling complication to the otherwise simple TypeScript compiler model. For these reasons, we recommend avoiding enumerations and using union types instead.
Why are type-level extension rules important?
Let’s consider how this rule interacts with the JavaScript ecosystem and TypeScript tools. TypeScript projects are JavaScript projects at heart, so they often use JavaScript build tools like Babel and Webpack. These tools were designed for JavaScript, which is still their main focus today. Each tool has its own ecosystem. There is a seemingly endless universe of Babel and Webpack plug-ins to handle code.
How can Babel, Webpack, their many plug-ins, and all the other tools and plug-ins in the ecosystem fully support TypeScript? For most features in the TypeScript language, type-level extension rules make these tools relatively easy. These tools simply strip the type comments away, leaving valid JavaScript code.
When it comes to enumerations (and namespaces, more on that later), things get harder. It is not enough to simply delete enumerations, these tools must add enum HttpMethod {… } to working JavaScript code, even though there is no enumeration in JavaScript to begin with.
This brings us to the real problem of TypeScript violating its own type-level extension rules. Tools like Babel, Webpack, and their plug-ins were first designed for JavaScript, so supporting TypeScript is just one of their many features. Sometimes support for TypeScript doesn’t get the same attention as support for JavaScript, which can lead to bugs.
Most tools do a good job of variable declarations, function definitions, and so on; All of this is relatively easy to deal with. But sometimes errors occur in enumerations and namespaces because they require more than stripping type comments. You can trust the TypeScript compiler itself to compile these features correctly, but some of the lesser-used tools in the ecosystem can make mistakes.
It can be very difficult to debug when your compiler, Bundler, Minifier, Linter, code Formatter, etc., silently miscompile or silently misinterpret code in a remote part of your system. Compiler errors are notoriously hard to track. Note this passage: “During the week, with the help of my colleagues, we managed to get a better idea of the scope of the error.”
2. Avoid namespace
Namespaces are like modules, except that there can be multiple namespaces in a file. For example, we might have a file that defines separate namespaces for its exported code and tests. (We don’t recommend doing this, but it’s an easy way to show namespaces.)
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();
Copy the code
Namespaces can cause problems in practice. In the enumeration section above, we saw TypeScript’s type-level extension rules. Typically, the compiler removes all type comments, leaving valid JavaScript code.
Namespaces break the type-level extension rule in the same way as enumerations. In namespace Util {export function wordCount… }, we cannot remove the type definition. The entire namespace is a typescript-specific type definition! Other code outside the namespace calls util.wordcount (…) What happens? If we delete the Util namespace before generating the JavaScript code, Util doesn’t exist, so util.wordcount (…) Function calls will not work.
As with enumerations, the TypeScript compiler cannot simply remove the definition of a namespace. Instead, it must generate new JavaScript code that doesn’t exist in the original TypeScript code.
For enumerations, we recommend using union types instead. For namespaces, we recommend using regular modules. Creating lots of small files can be a bit tedious, but modules have the same basic functionality as namespaces, with no potential drawbacks.
Step 3 Avoid decorators (temporarily)
Decorators are functions that modify or replace other functions (or classes). Here is an example of a decorator taken from official documentation.
// This is the decorator.
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t; }}Copy the code
The @sealed decorator above, which implicitly refers to C#’s sealed modifier, prevents other classes from inheriting from the sealed class. We do this by writing a sealed function that takes a class and modifies it to prevent inheritance.
Decorators were first added to TypeScript, and then standardization in JavaScript (ECMAScript) began. Decorators are still ECMAScript phase 2 proposals as of January 2022. The second stage is the “draft”. The decorator proposal also seems to be stuck in the committee’s purgatory: it has been in phase two since February 2019.
We recommend avoiding decorators until they are at least stage 3 (” candidate “) proposals, or stage 4 (” done “) for more conservative teams.
It is possible that ECMAScript decorators will never be finalized. If so, they end up in a similar situation to TypeScript enumerations and namespaces. They will always violate TypeScript’s type-level extension rules, and they are more likely to fail when using build tools other than the official TypeScript compiler. We don’t know if this will happen, but the benefits of decorators are small enough that we’d rather wait and see.
Some open source libraries, most notably TypeORM, make heavy use of decorators. We realized that following the recommendations in this article would rule out using TypeORM. Using TypeORM and its decorators is a good choice, but it should be intentional: recognizing that decorators are currently in the purgatory of standardization and may never be finalized.
Avoid using private
TypeScript privates class fields in two ways. One is the old private keyword, which is specific to TypeScript. Then there’s the new #somePrivateField syntax, which references JavaScript. Here is an example showing them separately:
class MyClass {
private field1: string;
#field2: string; . }Copy the code
We recommend the new #somePrivateField syntax for a simple reason: the two features are roughly equivalent. Unless there is a compelling reason, we want to maintain functional consistency with JavaScript.
Review our four suggestions:
- Avoid enumerations
- Avoid namespaces
- Pause using decorators until they are standardized. If you really need a library that requires decorators, consider their standardized status when making your decision
- support
#somePrivateField
Rather thanprivate somePrivateField
Even if you avoid these features, it’s good to have some knowledge of them. They often appear in traditional code, and even in some new code. Not everyone agrees to avoid them.
The original link
www.executeprogram.com/blog/typesc…