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.