Announcing: A Free Book, A New Course, A Huge Price Cut...
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
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:
dev
script to run our code locally and check for TypeScript errors.build
script to bundle our code for production and check for TypeScript errors.start
script to run our bundled code in production.package.json
Let's start with an empty repository and initialize it with npm init -y
. This will create a package.json
file.
"type": "module"
in package.json
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.
.gitignore
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
.
src
folderCreate 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!");
build
scriptAdd 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.
start
scriptAdd 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.
dev
scriptThe 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
.
tsc --watch
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.
node --watch
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
.
dev
scriptAdd 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.
Share this article with your friends
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
Learn why the order you specify object properties in TypeScript matters and how it can affect type inference in your functions.
Learn how to use corepack
to configure package managers in Node.js projects, ensuring you always use the correct one.
Learn how to strongly type process.env in TypeScript by either augmenting global type or validating it at runtime with t3-env.
Discover when it's appropriate to use TypeScript's any
type despite its risks. Learn about legitimate cases where any
is necessary.
Learn why TypeScript's types don't exist at runtime. Discover how TypeScript compiles down to JavaScript and how it differs from other strongly-typed languages.