dev/notes
⌕

Spot a mistake? Highlight any text in a post and click Report — it goes straight to the author.

← all posts
intermediate · TypeScript · August 17, 2026 · 3 min read

How to Set Up a TypeScript Project from Scratch

Create a TypeScript project from scratch: tsconfig.json, strict mode, and build scripts. A hands-on setup guide for Node.js and TypeScript.

Why strict mode from day one

TypeScript’s value is catching mistakes before runtime — but only if strict is on. Turning it on later means fixing a wall of errors at once. Enable it now, when there’s no legacy code to migrate.

Step 1 — Initialize the project

mkdir my-ts-project && cd my-ts-project
npm init -y
npm install -D typescript tsx @types/node
  • typescript — the compiler.
  • tsx — runs .ts files directly during development.
  • @types/node — types for Node’s standard library.

Step 2 — Create tsconfig.json

npx tsc --init

Replace the generated file with a focused config:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

noUncheckedIndexedAccess makes array/object access return T | undefined, forcing you to handle missing keys explicitly.

Step 3 — Write your first module

src/index.ts:

interface Greeting {
  name: string;
  punctuation?: string;
}

export function greet(input: Greeting): string {
  const mark = input.punctuation ?? "!";
  return `Hello, ${input.name}${mark}`;
}

console.log(greet({ name: "devnotes" }));

Step 4 — Path aliases for clean imports

Deep relative imports (../../../../lib/db) get unreadable fast. Add a paths map to tsconfig.json and import from an alias instead:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

// now: import { db } from "@/lib/db";

TypeScript resolves the alias at compile time, and tsx handles it during development. Node itself needs a resolver too — tools like tsx and bundlers cover this, so you only hit friction if you run compiled output with plain node.

Step 5 — Add build and dev scripts

In package.json:

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

npm run dev hot-reloads during development; npm run build typechecks and emits plain JavaScript to dist/.

Step 6 — Editor integration

The whole point of a strict tsconfig.json is catching errors before you run anything, and that happens in the editor. Open the project in VS Code (it ships TypeScript support built in) and you get live red squiggles, hover types, and go-to-definition — all driven by this same config. If errors don’t show, run TypeScript: Select TypeScript Version and pick the workspace version, so the editor uses your installed compiler instead of its bundled one.

Step 7 — Wire typechecking into CI

Make type errors fail the build:

npm run build

tsc with strict is your first line of defense — treat any error as a bug, not a suggestion.

Troubleshooting

  • Cannot find module with ESM — with "module": "NodeNext", local imports need explicit .js extensions (import { greet } from "./index.js").
  • Object is possibly undefined — that’s strict/noUncheckedIndexedAccess doing its job; narrow with a guard or optional chaining.
  • Output directory is messy — confirm rootDir/outDir are set so tsc mirrors your src/ layout.

Summary

You have a strict, modern TypeScript project with a dev loop (tsx watch) and a production build (tsc). The compiler is now guarding every commit.

For the editor side of this setup, see How to Set Up VS Code for Web Development; to make CI fail on type errors automatically, GitHub Actions for CI/CD wires npm run build into every push.

Related posts

Comments

One comment per thread every 30 minutes · edits are unlimited.