Using Typescript in node.js scripts without actually writing Typescript
Typescript is without a doubt my favorite way to write Javascript in 2024. However, there are times when I don't want to actually have to run tsc
or tsc --watch
to do basic scripting tasks, but I also really want to have type safe Javascript, AND be able to use ES Modules.
A few innovations both on the Typescript side, and the node.js side in the last few years have made it way easier to have your cake and eat it too when it comes to writing typesafe Javascript AND getting to use modern ES modules instead of commonjs!
Use jsdoc for the types
JSDoc has been around for quite a while now. I remember using it years ago to generate API docs when I built Backbone.js apps.
It's very similar in concept to what you'd use in C# apps as well, where you describe what a function or variable does by simply writing a code comment above it.
/**
* Debounces a given function
* @param {function} func function to debounce
* @param {number} time how long to debounce calls to func for (in milliseconds)
* @returns { function } debounced function
*/
export function debounce(func, time) {
// ...
}
You can use the @type
to simply set a given JS var as a type.
export function debounce(func, time) {
/** @type { NodeJS.Timeout | null } */
let timeout = null;
// ...
}
You can even define custom types using @typedef
.
/**
* @typedef { Object } NpmrcOrg
* @property { string } feed
* @property { string } organization
* @property { string } [pat]
*/
/** @type { Array<NpmrcOrg> } */
const defaultFeeds = [
{
organization: "foo",
feed: "https://foo.dev.azure.com/foo/bar/_packaging/foo/npm/registry/",
},
];
Using the square brackets marks a field as optional, both in @property
and in @param
like [pat]
in the above example.
Here we're declaring a type called NpmrcOrg
and then using it immediately below.
You can also reference types from other files, and even from NPM packages using @import
/**
* Create the user npmrc given an array of registry urls and a token
* @param {object} options
* @param {Array<import("../../utils/pat.js").NpmrcOrg> } options.feeds
* @param {string} options.existingNpmrc
* @returns {string}
*/
const createUserNpmrc = ({ feeds, existingNpmrc }) => {
Or referencing a node module...
import https from "https";
/**
*
* @param {import("http").RequestOptions} options
* @returns
*/
export const makeRequest = async (options) => {
Here are the docs on all the possible things you can do with JSDoc.
Configure Typescript to check Javascript and allow ES Modules
There are a few important flags to have in your tsconfig.json
and package.json
to make this all work.
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "ESNext"],
"module": "ESNext",
"allowJs": true,
"checkJs": true,
"rootDir": "./src",
"outDir": "./lib",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": ["src"]
}
The checkJs
and allowJs
will instruct both the Typescript compiler, and VSCode to do typechecking on your JS and thus parse all the JSDoc comments into Typescript.
The module
being set to "ESNext"
is what tells TS that you're going to be using ESModules.
If you hate the import * as
stuff you can set "allowSyntheticDefaultImports"
and "esModulesInterop"
.
Running tsc
against that tsconfig will produce JS files in ./lib
along with the .d.ts
files created from parsing your JS files, and the JSDoc comments.
Lastly, in your package.json
if you want to, you can set "module": true
that will tell node
when you run it that you're using ES Modules, and as of a few node versions ago, the import
and export
of modules will just work by running node path/to/your/script.js
Read more about node.js's ES Modules and about how Typescript handles ES Modules as well.
This works so well in VSCode
The best thing ever is seeing the magic of all this working in VSCode. 😍
You get full intellisense and code completion like you would using native .ts files.
You get the goodness of Typescript without needing to do a single yarn build
or npm run build
or anything, because, guess what, you're using native Javascript!
Conclusion
Lots of open source tools have moved towards this model, including, but not limited to webpack, and svelte.
There are definitely still advantages to using full on .ts
extensions, and using the tsc
compiler, or swc
/ babel
etc to do the transpiling work, but it's nice to know that you don't have to go for a full typescript build for every part of your code base.
This type of programming works great for scripting where you just want to automate some tasks, do system level work, write a CLI, etc. The feedback loop is very fast because there's no compile step at all.
Go forth and write more native Javascript with type checking!