Skip to main content

Β· 3 min read
Homa Wong (unional)

TypeScript 4.7 is released πŸŽ‰!

The wait is finally over. You are excited to start your migration.

So you update your package.json, update your tsconfig.json, and voila, you are greeted with a boat load of errors.

What you need to do is to update your import statements:

  • add .js extension for files, or
  • add /index.js for folders.

You can do that manually, or you can use a tool to help you on that.

The tool (tsc-esm-fix) is the focus of the topic today. But before that, let me highlight a few things about the current state of the type: module support in TypeScript 4.7, so that you can decide should you take the jump right now, or should you wait a little bit longer.

There are four issues that I'm aware of related to type: module.

The .js extension debate​

The first one is a debate around the .js extension.

The takeaways are:

  • .js is artificial, pointing to a non-existing file
  • TypeScript team do not want to rewrite JS code
  • NodeJS needs extra tricks to detect the source type

You can read about the detail here:

broken source map​

The second one is that source map is currently broken.

If your code will be used in the browser, you need to weight that in.

You can read about the detail here: https://github.com/microsoft/TypeScript/issues/49335

ts-loader workaround​

The third one is that currently ts-loader does not support ESM module out of the box.

You have to do two things.

  1. use the NormalModuleReplacementPlugin:
{
plugins: [
new NormalModuleReplacementPlugin(/.js$/, (resource) => {
if (/node_modules/.test(resource.context)) return
resource.request = resource.request.replace(/.js$/, '')
})
]
}
  1. Do not use transpileOnly: true. It does not work.

If I have time, I might dig in to help ts-loader about this, but not sure when I'll be able to do that.

You can read about the detail here: https://github.com/TypeStrong/ts-loader/issues/1463

jest ESM workaround​

The last one is an outstanding one, that jest does not have native support of ESM.

Meaning when you use ts-jest, you also need to use babel-jest to transpile dependencies within node_modules.

Here is a nutshell of what you need to do:

// jest.config.mjs
export default {
preset: 'ts-jest/presets/default-esm',
globals: {
'ts-jest': {
useESM: true
}
},
moduleNameMapper: {
// remove the phantom `.js` extension
'^(\\.{1,2}/.*)\\.js$': '$1',
// If dependency doing `import ... from '#<pkg>'.
// e.g. `chalk` has this: `import ... form '#ansi-styles'`
'#(.*)': '<rootDir>/node_modules/$1'
},
transformIgnorePatterns: [
// Need to MANUALLY identify each ESM package, one by one
'node_modules/(?!(@unional\\fixture|chalk)/)'
],
transform: {
'^.+\\.(js|jsx|mjs)$': 'babel-jest',
}
}

You can find related information here: https://dev.to/steveruizok/jest-and-esm-cannot-use-import-statement-outside-a-module-4mmj

tsc-esm-fix​

Now, if you decide to move ahead, then you can use tsc-esm-fix to help you.

I used it to help me migrating type-plus, which has 140+ files. I'm using it for other packages as I'm writing this blog.

I have discovered a few bugs and @antongolub is very quick to fix them. Go to the repo and give it a star ⭐!

The easiest way to use it is through npx or yarn dlx:

npx tsc-esm-fix --src=<src> --ext='.js'

# or
yarn dlx tsc-esm-fix --src=<src> --ext='.js'

For type-plus, since I put the source code under the ts folder, so the command is:

npx tsc-esm-fix --src=ts --ext='.js'

If you put your source code under the typical src folder, then the command is:

npx tsc-esm-fix --src=src --ext='.js'

Happy Coding, πŸ§‘β€πŸ’»

Β· 3 min read
Homa Wong (unional)

ESM, or ECMAScript Module, was original part of the ES6/ES2015 specification. It turned out to be difficult to implement and was pulled from the core specification at the last minute.

There were many discussions and argument about it. At one point, it almost falls apart because NodeJS couldn't get to a good design to get CommonJS and ESM to co-exist.

Fast-forward a few years, ESM is mature enough to be used on both NodeJS and browsers.

By now, I'm pretty sure you have heard of it already.

But for people who write code in TypeScript, while we use import and export for a very long time, we were not able to publish the code as ESM.

What's worse is that, some JavaScript code moved on to ESM and the new module package in NodeJS, and TypeScript code cannot consume them.

So the TypeScript code is being left behind.

TypeScript 4.5 planned to support the new module system, but the support was pulled due to some cases were not resolved.

While it was heartbreaking when the announcement was made, I completely understand and support the rationale behind it.

It would be much worse if the solution is half-baked and released to the world. The damage it causes could be devastating.

TypeScript 4.7 is trying to provide the support again, and hopefully it's here to stay.

The big migration​

So how to migrate your code to ESM?

Upgrade TypeScript:

npm install --dev typescript@rc

yarn add -D typescript@rc

(or typescript without @rc when 4.7 is released).

Update your package.json to:

{
"type": "module",
"exports": {
".": {
"import": {
"default": "<path to main>",
"types": "<path to d.ts>"
}
}
}
}

In TypeScript 4.7 announcement and other places mentioned you can provide fallback for CommonJS, but in general it could cause problems as that will increase the chance of having multiple copies of your library exist in runtime.

Update your import statements to add .js extension. e.g.:

// import { foo } from './foo'
import { foo } from './foo.js'

Update your project files from .js to either .cjs or .mjs.

When you set the package to use "type": "module", Your project files such as jest.config.js and babel.config.js becomes ambiguous, and you have to update the file extension to reflect if the file is a CommonJS or ESM file.

Some tools might not work with the new module format. You have to disable them for now.

To me, I found that size-limit does not support it yet.

Also, I found that I can't import the default export from TypeScript package transpiled to CommonJS.

Don't know if that is by design or it is a bug yet.

You can take a look at the global-store PR as an example.

Happy Coding, πŸ§‘β€πŸ’»

Β· One min read
Homa Wong (unional)

Recently I run into this problem:

ESLint couldn't determine the plugin "@typescript-eslint" uniquely.

- ...\node_modules\@typescript-eslint\eslint-plugin\dist\index.js (loaded in ".eslintrc.json")
- ...\node_modules\eslint-config-react-app\node_modules\@typescript-eslint\eslint-plugin\dist\index.js (loaded in ".eslintrc.json Β» eslint-config-react-app#overrides[0]")

It is caused by eslint-config-react-app using @typescript-eslint/eslint-plugin as a dependency instead of a peer dependency.

Here is the GitHub issue in eslint-config-react-app (also here).

Plugin and dependency​

The problem is when one plugin uses another plugin, it should always declare the dependency as a peer dependency.

The reason is simple.

The host application (ESLint in this case) controls and loads its plugins. If the host application is not designed to support loading multiple versions of the same plugin at the same time, which most of them don't, then the result is an undefined behavior.

That's why ESLint plainly detects and disallows it.

It also mentioned it in its doc.

Yes, that means the consuming repository needs to add the dependency themselves. Any version incompatibility in the dependency graph would lead to the doppelgΓ€ngers problem.

That is an inherited problem of NodeJS resolution algorithm, and is a topic for another day.

Happy coding, πŸ§‘β€πŸ’»

Β· 4 min read
Homa Wong (unional)

TypeScript 4.7 is just around the corner.

I feel like it is a good time for me to update this TypeScript guidelines with what I have learn over the years and how am I using it nowadays.

As you can see, I've reorganized the repository and added this GitHub page to make it easier to share the knowledge.

In these blog posts, I'm going to share the bits and tips that I learned every day. Eventually this information will be incorporated into the guidelines.

To kick things off, I'm going to talk about what TypeScript is in 2022?

No no, not about the history of TypeScript.

It's about how I look at TypeScript and how I use it.

It's about when I follow the rules and when I break them.

I know all the rules, and then I know how to break 'em

TypeScript is an abstraction​

Programming language is an abstraction.

The abstraction I'm talking about is not the abstraction of defining an interface or class or inheritance.

It's about the code you wrote, the text you typed in the .ts or .tsx file.

It is an abstraction over the actual binary code executed by the JavaScript engine.

A good abstraction emphasizes the important but sometimes has a cost of skipping over some details.

For example, the digital zeros and ones are an abstraction over the analog electrical currents.

Most of the time, at least in the sense of modern computing, missing the details on the different levels of electrical currents is a good thing. And we as a programmer don't ever need to think about it.

But to the eye of hardware engineer, it is not something they can ignore because there are physical rules they have to follow, and there are actual consequences when putting two wires too close to each other.

The abstraction of programming language is the same. It is good for most cases, but at some point it will start to breakdown.

rust is a good example of this. Its ownership model guarantee memory-safety and thread-safety. But there are rare cases you need to break the rules thus there is a unsafe keyword for it.

For TypeScript, it is not designed to be a 100% sounded language. That means you will run into those "rare" cases a lot more often.

You can learn more about soundness here and here.

Conclusion​

So what should you do? Embrace it.

The unsafe keyword in TypeScript is any. While you should avoid using any in most cases, it is often enough that your tool chain should not prevent or punish you from using it.

You might think,

"hey, I'm working in a team and I don't want my team members who are less familiar with TypeScript to abuse the usage of any."

Yes, that is a legit concern, and you can make your own judgement for your circumstances.

My personal experience is that the number of times I have to break the rules are common enough that making exceptions for the tools (e.g. adding // eslint ignore comments) clutter the code and make the code harder to read.

My approach is to rely on unit tests and code reviews to make sure the usage of any are in check.

Also, defining the correct type is not always easy and can take significant amount of time. If you are working under a schedule, you might not have time to do that.

Another point to make is that since TypeScript is constantly improving, if you build some complex types, it might work in one version and break in another version.

ASK ME HOW I KNOW IT.

Of course, I'm not saying allowing your code litters with any and call it a day. You should keep track on those usages and clean it up when you get a chance to.

Converting any back to a proper type improves efficiency. As that is why we use TypeScript.

And you may discover some bugs along the way.