ยท hands on
Write a simple TypeScript script with ESM
I recently wrote a small TypeScript script to generate a Markdown file with a sluggified filename. Since we're now in the era of modern ECMAScript Modules (ESM), I wanted to use this new module system in my TypeScript code. Here's how I did it.
I installed ts-node so I could run my script in a Node.js environment from the command line. The great thing about ts-node
is that it works well with ESM. It comes with a binary called ts-node-esm
that supports loading ECMAScript modules when you run TypeScript code.
Contents
Making TypeScript Work with ESM
Once you've installed ts-node
with npm install
, you can immediately direct it to your script by:
npx ts-node-esm src/main.ts
This ensures that an ECMAScript loader is ready when your code is executed.
Handling File and Folder Paths
In the world of ESM, the usual __filename
and __dirname
constants are not available. So, I found a way to mimic this by using the import.meta property:
import path from 'node:path';
import url from 'node:url';
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Using New File Extensions
TypeScript lets you pick different file extensions to tell whether you're using ES modules (.mts
) or CommonJS modules (.cts
).
If you only need to run a single script with the ES module syntax, I recommend using the .mts
extension. It's great in scenarios where you don't want to set up your whole project to use ES modules.
Demo Code
Here's the complete script I developed:
import GithubSlugger from 'github-slugger';
import fs from 'node:fs';
import path from 'node:path';
import url from 'node:url';
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = process.argv.slice(2);
const title = args[0];
const slugger = new GithubSlugger();
const slug = slugger.slug(title);
const filePath = path.join(__dirname, 'content', 'blog', `${slug}.md`);
console.log(`Creating file "${filePath}"...`);
fs.writeFileSync(filePath, '', 'utf8');
console.log(`Created file "${filePath}".`);
It can be ran as follows:
npx ts-node-esm src/new-post.mts "My Post Title"
There is the possibility that this error occurs when using Node.js v20:
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for src/start.mts
This is because ts-node v10.9.2 doesn't work well with Node.js v20 and ECMAScript modules (see GitHub issue).
Luckily, there is a solution. You just have to make use of the --loader
flag of Node.js:
node --loader ts-node/esm src/new-post.mts "My Post Title"
Unfortunately, this workaround doesn't provide type checking as it is broken with ts-node v10.9.2, so your error output will appear as follows:
node:internal/process/esm_loader:40
internalBinding('errors').triggerUncaughtException(
^
[Object: null prototype] {
[Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]]
}
To fix this, run type checking directly through TypeScript's compiler using the noEmit
flag to prevent unintentional output:
tsc --noEmit && node --loader ts-node/esm src/new-post.mts "My Post Title"
You may see an experimental warning because the --loader
flag is likely to be removed in future Node.js versions. To hide this message, use the following command:
tsc --noEmit && node --no-warnings=ExperimentalWarning --loader ts-node/esm src/new-post.mts "My Post Title"
For a full explanation, check out the video I made here: