If we truly love and understand a language, we should not just embrace the good parts of it, but also know about its bad parts, and try to avoid them as much as we can.
In this article, I’m going to talk about the many bad designs and “features” that I and many developers know of (but won’t talk about) that have been brought into JS and its ecosystem in recent years, including the JavaScript/TypeScript language, runtimes, and even famous third-party libraries.
To be clear, I won’t talk about some very old topics like NaN !== NaN
, that kind of thing, they are some of the historical burdens of the language, and it’s not just JS, so many other languages share something alike too, just live with it. Moreover, not everybody shares the same opinion about the same thing, so, 🤷.
Private class members
Every year, ECMA brings new features to the JS language, many of them are very good and useful, but not always. One of them is the private class members. Well, having private members is good, we can use it to hide implementation details we don’t want to expose to outsiders. What’s bad is the design. The following example shows what the private members look like:
class Example {
#foo = "foo"; // private field
// private method
#log() {
console.log(this.#foo);
}
bar() {
this.#log();
}
}
const ex = new Example();
ex.bar();
JavaScriptSo what is wrong with the design, or more specifically, the use of #
. Well, there are two major reasons why it’s bad, apart from looking odd.
- In some other languages, including Python and Bash Script, even in the executable JS script header,
#
is used for comments, using it to annotate a symbol causes so much confusion. #
has widely adopted in documentation to describe the prototype/instance members of a class, Something likeFoo#bar()
is familiar to many people’s eyes. I don’t have to argue strongly that now using#
for another meaning will cause what kind of problems.
It would be nicer if we use @
instead. Why? Because, when we talk about symbol primitives, such as Symbol.iterator
, we often use the term @@iterator
, so it’s just natural that we describe something similar with the same symbol. But what’s done is done, 🤷,stick with TypeScript and use the private
modifier instead.
String trimming functions
There are three functions to trim spaces in a string, trim
, trimStart
and trimEnd
. These functions are certainly doing a great job of removing leading and tailing spaces, but what they lack is the ability to specify custom characters to strip off, resulting in that we have to rely on third-party libraries such as lodash to provide this functionality, which many other languages provide in their APIs out of the box.
JS could just give an optional secondary argument to these functions, like many other built-in functions, such as slice
, padStart
and padEnd
, I cannot think of why the padding functions have this option but not the trimming functions, just can’t.
Moment’s month method
Moment.js is a widely used library to manipulate dates and times. Many of its features are great, except for one thing, the month()
method returns 0
instead of 1
for January.
Some people may not know, that Date#month()
returns 0
, which JS borrowed from Java, has been considered a historical design flaw. But Java has fixed it by introducing a new java.time package since Java 8.
Moment.js, a library that was meant to ease the work of dealing with dates and times, should have fixed the 0
indexing problem like the java.time library, but it hasn’t, which makes this library somewhat unfriendly to the modern developers’ world.
Node.js ESM import CJS
Suppose we have the following code in TypeScript:
// log.ts
export default function log(text: string) {
console.log(text);
}
TypeScriptWhich will be transpiled to JavaScript like this:
// log.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function log(text) {
console.log(text);
}
exports.default = log;
JavaScriptAnd we have the following program that imports this file:
import log from "./log.js";
log("Hello, World");
JavaScript or TypeScriptIf we are going to run this script with ts-node
or bun
, it will work as expected. But if we’re trying to run it with node
, sorry,
TypeError: log is not a function
BashIt turns out that Node.js doesn’t support the exports.default
and exports.__esModule
at all, and treat module.exports
as default export instead. This is the worst design of Node.js ES module system I can think of. Just because the Node.js team refused to accept the fact that people have been using TypeScript this way for many years, and has become the de facto industrial standard.
This odd behavior has greatly held back the transition from CJS to ESM in Node.js, because it breaks the many codebases generated by TypeScript. Unless library authors rewrite and regenerate all their code in ESM format (unlikely), CJS will remain in the foreseeable future for a very long time.
If Node.js honors exports.default
and exports.__esModule
, we will never need to worry about the CJS dependencies and just code new applications and libraries in ESM all along.
So, Bun to the rescue! It solves many problems that Node.js refuses to solve. In my opinion, Bun is the true future of Node.js. Or we could just use TypeScript with esModuleInterop
and never write pure JS again.
Deno import npm
When Deno first came out, I’m sure many people were as excited as I was. But as time went by, we realized that totally leaving the Node.js ecosystem and the massive NPM packages isn’t realistic. Not only do library authors refuse to write libraries for Deno, but TypeScript itself, the very foundation of Deno, also refuses to support the .ts
extension name in the import statement.
In the end, Deno has to do backward compatibility itself, by allowing people to import NPM packages from the node_modules via a npm:
prefix. But this method, I cannot say it’s badly designed, but rather, incomplete and still not fully compatible with Node.js, because Node.js doesn’t allow npm:
prefixes. So if we’re thinking about running the code with npm:
prefixes in Node.js, sorry, no, can’t do.
Another thing is, what’s the point of supporting node_modules but not supporting .d.ts
files in the packages? Because Deno doesn’t recognize the type declaration files, using npm packages in Deno can be very dangerous because there will be no types and no autocompletion provided by the IDE at all, and we will be going back to the JS hellhole we got out many years ago.
It’s hard to imagine, that Deno, something that is meant to be strongly type-safe in the first place, results in a totally unsafe situation like this. Again, Bun to the rescue!
TypeScript await using
Actually, there is an ECMA proposal about this, but TypeScript has supported it now since v5.2. It’s so disappointing that JS/TS added this to the language instead of adding something similar to Golang’s defer
functionality. Take a look at the following code, and tell me it’s not confusing:
await using resouce = new AsyncDisposableResource();
for (await using x of y) {
// ...
}
for await (await using x of y) {
// ...
}
TypeScriptDo these code lines await or not? Yet another and even worse design since private class members. It would’ve been much better if we used defer
instead, like this:
const resouce = openResouce();
defer await resource.release();
for (const x of y) {
defer await x.close();
// ...
}
for await (const x of y) {
defer await x.close();
// ...
}
TypeScriptI have another article <<TypeScript has ‘using’ now, but we should avoid using it> complaining about this in more detail, check it out if you like.
These are some of the bad decisions I can think of right now, feel free to share others you have found.