In this article, we'll learn how to set up TypeScript to bundle a Node app.
We'll be using:
If you're interested in a setup involving ESBuild, check out my ESBuild guide.
#
To make our Node app ready for production, we will need a few things:
- A
dev
script to run our code locally and check for TypeScript errors.
- A
build
script to bundle our code for production and check for TypeScript errors.
- A
start
script to run our bundled code in production.
#
#
Let's start with an empty repository and initialize it with npm init -y
. This will create a package.json
file.
#
Next, add "type": "module"
to the package.json
file.
{
// ...other properties
"type": "module"
// ...other properties
}
This tells Node.js to use ES Modules instead of CommonJS modules.
#
If you don't have pnpm installed, install it.
Next, let's install our dependencies:
pnpm add -D typescript @types/node
This will add typescript
and @types/node
to our package.json
.
This will also create a pnpm-lock.yaml
. This file keeps track of the exact versions of our dependencies to make installation faster and more predictable.
#
Add a tsconfig.json
file at the root of the project with the following configuration:
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
/* If transpiling with TypeScript: */
"moduleResolution": "NodeNext",
"module": "NodeNext",
"outDir": "dist",
"sourceMap": true,
/* If your code doesn't run in the DOM: */
"lib": ["es2022"]
}
}
This configuration is drawn from Total TypeScript's TSConfig Cheat Sheet.
One important option to note is moduleResolution
: this ensures that TypeScript uses the same module resolution as Node.js. If you're not used to it, this might be surprising - as you need to add .js
extensions to your imports. But using it massively improves the startup time of your Node app, which is very important for lambdas.
#
Add a .gitignore
file with the following content:
node_modules
dist
node_modules
contains all of the files we get from npm
. dist
contains all of the files we get from tsc
.
#
Create a src
folder at the root of the project.
Inside the src
folder, create an index.ts
file with the following content:
console.log("Hello, world!");
#
#
Add a build
script to package.json
:
{
// ...other properties
"scripts": {
"build": "tsc"
}
// ...other properties
}
This script turns our TypeScript code into JavaScript using tsc
, and also checks for any errors.
Try changing console.log
to console.lg
in src/index.ts
. Then run pnpm build
- it will report the incorrect code. It'll also output a .js
file in the dist
folder.
#
Add a start
script to package.json
:
{
// ...other properties
"scripts": {
"start": "node dist/index.js"
}
// ...other properties
}
This script runs our bundled code using Node.js.
Try running pnpm build && pnpm start
. This will build our code and then run it.
You should see Hello, world!
printed to the console.
#
The dev
script will be the most complex. When we run it, we want to do several things at once:
tsc --watch
to bundle our TypeScript code and check for errors.
node --watch
to re-run our application when it changes.
For each of these, we will add a separate npm
script, then run them all simultaneously using pnpm
.
#
Add a dev:tsc
script to our package.json
:
{
// ...other properties
"scripts": {
"dev:tsc": "tsc --watch --preserveWatchOutput"
}
// ...other properties
}
The --watch
flag tells TypeScript to re-run when the code changes.
The --preserveWatchOutput
flag tells TypeScript not to clear the console output when it re-runs.
#
Add a dev:node
script to our package.json
:
{
// ...other properties
"scripts": {
"dev:node": "node --enable-source-maps --watch dist/index.js"
}
// ...other properties
}
--enable-source-maps
means that error stack traces will point to your TypeScript files instead of your JavaScript files. This is possible because of the "sourceMap": true
in our tsconfig.json
.
#
Add a dev
script to our package.json
:
{
// ...other properties
"scripts": {
"dev": "pnpm run \"/dev:/\""
}
// ...other properties
}
This script runs all the scripts that start with dev:
in parallel.
Try it out by running pnpm dev
. You will see that type checking, bundling, and execution all happen simultaneously.
#
Congratulations! You now have a fully functional TypeScript and Node setup.
This setup can handle any Node.js code you throw at it, from express
servers to Lambdas.
If you want to see a fully working example, check out this repository.
If you have any questions, ping me in my Discord server and I'll let you know how to fix it.