Introduction
I should start by saying that I am a big proponent of TypeScript. I have been using it since version 1.5 back in 2015. Coming from a backend background with typed languages, not having types in JavaScript was an interesting experience for me. As a developer, the two aspects of typed languages that help me the most are intellisense and the compiler. Intellisense helps me write code faster, and the compiler helps me catch errors before runtime.
While JavaScript itself doesn’t have a type system, the tooling around it has significantly improved over the years. For example, VS Code has an excellent IntelliSense engine that can infer types from the code. Additionally, you can enhance the development experience even further by leveraging TypeScript’s type system, without actually writing TypeScript code. With this in mind, I decided to try converting one of my TypeScript projects to pure JavaScript using JSDoc. For this exercise, I chose sharp-recipe-parser because it is a small npm package library that doesn’t explicitly require a build system.
In the next sections, we will take a somewhat detailed look at the process, challenges, and the more interesting aspects of the conversion. Note that this is not necessarily a conversion guide, but rather how I approached it in this instance.
TLDR;
Here is the commit that includes 99% of the changes: commit link.
1. Change .ts to .js
Easy enough. Just change all .ts
files to .js
. Obviously, you will get a ton of errors on any type notations you have in the code. We will fix that next.
2. Convert type annotations to JSDoc
Funny enough, we are converting TypeScript code to JavaScript, but not removing TypeScript from the project. We can still use it to do some type checking and to enhance the coding experience. So, we add the //@ts-check
notation to the top of each .js
file. This will greatly enhance your experience in IDEs that support this flag, such as VS Code. You can read more about it here.
Next, replace TypeScript-specific notations with JSDoc. For example, replace : string
with @type {string}
. You can find a list of the notations here.
Some types are used in different modules. For this reason, I created a types.js
module that only hosts a few common types in JSDoc format. This file is then imported in the other files as needed.
Partial types.js
file:
|
|
When consuming:
|
|
3. JavaScript modules
In TypeScript, I nearly always do a module import without the extension. During transpilation, TypeScript will adjust the import as needed. However, browser environments will require the full file name. For example, import { tokenize } from "./tokenizer";
will need to be changed to import { tokenize } from "./tokenizer.js";
.
Additionally, to keep using import instead of require, change the package.json
file to use the type = module
flag. This will, however, create a few other issues in tooling to be explored in the next section.
4. Tooling changes
At this point, the code itself should work. But there are problems with unit tests and linting. We will need to make some changes to our tooling to get it to work. We’ll start by removing some npm packages that are no longer needed:
|
|
4.1. Jest
Jest currently provides experimental support for ESM modules. However, I found that using Babel was an easier experience. You can find a good guide on how to do that here. In summary:
- Uninstall some packages:
|
|
- Install babel:
|
|
- Create
babel.config.cjs
:
Note that
.cjs
is used because the Babel config uses CommonJS syntax.
|
|
- Rename the config file to
jest.config.cjs
and adjust the coverage path:
Note that
.cjs
is used because the Jest config uses CommonJS syntax.
|
|
4.2. Eslint
Make the following changes to the .eslintrc.js
file:
- Remove TypeScript-specific plugins and replace with
eslint:recommended
.
|
|
- Change parser options to use the latest ECMAScript version and source type module. Also change env to use browser, node, ES6, and Jest.
|
|
- Add a new plugin to help with JSDoc integration.
|
|
5. Generating TypeScript types
At this point, sharp-recipe-parser was converted. However, any TypeScript-based clients, like Sharp Cooking, would not have the necessary types for compilation. We can generate them using the tsc
command. This will leverage the new JSDoc to generate a .d.ts
file for each .js
file in the src
directory.
|
|
At this point, we can run this once after every change or change the GitHub workflow to run this command before packaging and pushing to npmjs.com. Since it is very likely I will forget to run this command, I opted for the latter.
Add the command to package.json:
|
|
Add the command to the GitHub workflow:
|
|
Conclusion
I honestly thought the process would be easier than it actually was. Converting the code and creating the JSDoc entries wasn’t bad. But making modules, ESLint, and Jest work was a bit more challenging. I do expect that creating a new project from scratch would be easier than going through the conversion process.
While I do like the simplicity of JavaScript instead of TypeScript for libraries, so far the benefit has been fairly small. I expect to have a better perspective as I make changes to sharp-recipe-parser. For other projects, I will definitely stick to my old faithful TypeScript for apps and evaluate on a case-by-case basis for libraries.