javascriptroom guide

Scaffolding TypeScript Projects with Modern Tooling

TypeScript has become the backbone of modern JavaScript development, offering type safety, improved tooling, and better scalability. However, setting up a TypeScript project from scratch can be daunting—choosing the right build tools, bundlers, linters, and formatters often feels like navigating a maze. This blog demystifies the process of scaffolding TypeScript projects using modern tooling. Whether you’re building a frontend app, a Node.js backend, or a full-stack monorepo, we’ll cover everything from basic setups to advanced configurations. By the end, you’ll confidently create production-ready TypeScript projects tailored to your needs.

Table of Contents

  1. Understanding the Modern TypeScript Ecosystem
  2. Scaffolding a Basic TypeScript Project with tsc
  3. Modern Scaffolding with Vite
  4. Scaffolding a Node.js TypeScript Project
  5. Integrating Essential Tooling (Linting, Formatting, Testing)
  6. Tsconfig.json Deep Dive
  7. Advanced: Scaffolding Monorepos with Turborepo
  8. Conclusion
  9. References

1. Understanding the Modern TypeScript Ecosystem

Before diving into scaffolding, let’s map the key tools that power modern TypeScript projects:

TypeScript Core

  • tsc: The official TypeScript compiler, converts TypeScript (.ts) to JavaScript (.js).
  • tsconfig.json: Configures the compiler (e.g., target JS version, module system, strictness).

Package Managers

  • npm: Default Node.js package manager.
  • Yarn: Faster alternative with deterministic installs.
  • pnpm: Space-efficient with a shared dependency cache (great for monorepos).

Build Tools & Bundlers

  • Vite: Next-gen frontend build tool (uses esbuild for fast HMR and Rollup for production bundling).
  • esbuild: Ultra-fast JavaScript/TypeScript bundler (used under the hood by Vite).
  • ts-node: Executes TypeScript files directly without pre-compiling (ideal for Node.js).

Linting & Formatting

  • ESLint: Identifies code errors and enforces style rules (with @typescript-eslint for TS support).
  • Prettier: Automatically formats code for consistency (works seamlessly with ESLint).

Testing

  • Vitest: Vite-native test runner (fast, ESM-first, TypeScript-friendly).
  • Jest: Popular testing framework (works with TypeScript via ts-jest).

2. Scaffolding a Basic TypeScript Project with tsc

Let’s start with the fundamentals: a minimal project using the official TypeScript compiler (tsc). This is ideal for learning or small scripts.

Step 1: Initialize the Project

First, create a project folder and initialize a package.json:

mkdir ts-basic-demo && cd ts-basic-demo
npm init -y  # -y skips prompts and uses defaults

Step 2: Install TypeScript

Install TypeScript as a dev dependency (we’ll use --save-dev since it’s a build tool):

npm install --save-dev typescript

Step 3: Configure tsconfig.json

Generate a tsconfig.json (TypeScript’s configuration file) with:

npx tsc --init

This creates a default tsconfig.json with hundreds of commented options. Let’s simplify it for our needs:

{
  "compilerOptions": {
    "target": "ES2020",        // Compile to ES2020 (modern browsers/Node.js)
    "module": "ESNext",        // Use ES modules (import/export)
    "outDir": "./dist",        // Output compiled JS to `dist/`
    "rootDir": "./src",        // Source TS files live in `src/`
    "strict": true,            // Enable all strict type-checking options
    "esModuleInterop": true,   // Allow `import` of CommonJS modules
    "skipLibCheck": true,      // Skip type-checking of .d.ts files (faster)
    "forceConsistentCasingInFileNames": true  // Avoid OS-specific casing issues
  },
  "include": ["src/**/*"],     // Include all TS files in `src/`
  "exclude": ["node_modules"]  // Exclude dependencies
}

Step 4: Write Your First TypeScript File

Create a src folder and add a main.ts file:

// src/main.ts
function greet(name: string): string {
  return `Hello, ${name}!`;
}

const user = "TypeScript Developer";
console.log(greet(user)); // Output: Hello, TypeScript Developer!

Step 5: Build and Run the Project

Add a build script to package.json:

{
  "scripts": {
    "build": "tsc",       // Compile TS to JS
    "start": "node dist/main.js"  // Run the compiled JS
  }
}

Now build and run:

npm run build   # Generates dist/main.js
npm start       # Runs the script

Output:

Hello, TypeScript Developer!

Limitations of this setup: No live reloading, slow builds for large projects, and no built-in bundling for browsers. For modern apps, we’ll use tools like Vite next.

3. Modern Scaffolding with Vite

Vite has revolutionized frontend development with its speed and simplicity. It uses esbuild for pre-bundling dependencies and Rollup for production builds, making it perfect for TypeScript projects.

Why Vite?

  • Fast HMR: Updates reflect instantly without full page reloads.
  • Optimized Builds: Rollup produces smaller, tree-shaken bundles.
  • Zero Config: TypeScript support out of the box (no manual tsconfig setup needed).

Scaffolding with Vite

Run the Vite starter command and follow the prompts:

npm create vite@latest my-vite-ts-app
  • Select a framework (e.g., Vanilla, React, Vue—we’ll use Vanilla for simplicity).
  • Choose TypeScript as the variant.

Project Setup

Navigate to the project and install dependencies:

cd my-vite-ts-app
npm install

Project Structure

Vite generates a clean structure:

my-vite-ts-app/
├── node_modules/
├── public/          # Static assets (e.g., images)
├── src/
│   ├── main.ts      # Entry point
│   ├── vite-env.d.ts # Type definitions for Vite
│   └── style.css
├── index.html       # HTML entry (Vite uses this to inject scripts)
├── package.json
├── tsconfig.json    # Pre-configured for Vite
└── vite.config.ts   # Vite-specific config

Run the Development Server

Start the dev server with:

npm run dev

Vite will launch a server at http://localhost:5173. Edit src/main.ts, and changes will appear instantly!

Build for Production

To bundle for production:

npm run build

Vite outputs optimized files to dist/, ready for deployment. You can preview the build with:

npm run preview

4. Scaffolding a Node.js TypeScript Project

For backend projects (APIs, CLIs), use ts-node to run TypeScript directly and nodemon for live reloading.

Step 1: Initialize and Install Dependencies

mkdir ts-node-demo && cd ts-node-demo
npm init -y
npm install --save-dev typescript ts-node nodemon @types/node
  • ts-node: Executes TypeScript without pre-compiling.
  • nodemon: Restarts the server on file changes.
  • @types/node: Type definitions for Node.js APIs.

Step 2: Configure tsconfig.json

Generate and update tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",  // Node.js uses CommonJS by default
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Step 3: Add a Server Script

Create src/server.ts:

// src/server.ts
import http from "http";

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello, Node.js TypeScript!");
});

const PORT = 3000;
server.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Step 4: Add Nodemon Scripts

Update package.json scripts:

{
  "scripts": {
    "dev": "nodemon --exec ts-node src/server.ts",  // Live reload
    "build": "tsc",                               // Compile for production
    "start": "node dist/server.js"                // Run compiled JS
  }
}

Start the dev server:

npm run dev

Visit http://localhost:3000—you’ll see Hello, Node.js TypeScript!. Edit server.ts, and nodemon will auto-restart the server.

5. Integrating Essential Tooling

Modern projects require linting, formatting, and testing. Let’s add these to our Vite or Node.js project.

Linting with ESLint + TypeScript

ESLint catches errors and enforces code style.

Install Dependencies

npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser

Configure ESLint

Create .eslintrc.js:

module.exports = {
  root: true,
  parser: "@typescript-eslint/parser", // Parse TypeScript
  plugins: ["@typescript-eslint"],    // TypeScript-specific rules
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended", // Recommended TS rules
    "plugin:@typescript-eslint/strict-type-checked", // Strict type-checking
  ],
  parserOptions: {
    project: "./tsconfig.json", // Use our tsconfig for type info
  },
};

Add a lint script to package.json:

"scripts": {
  "lint": "eslint src/**/*.ts"
}

Run linting:

npm run lint

Formatting with Prettier

Prettier ensures consistent code formatting.

Install Prettier

npm install --save-dev prettier eslint-config-prettier
  • eslint-config-prettier: Disables ESLint rules that conflict with Prettier.

Configure Prettier

Create .prettierrc:

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5"
}

Update .eslintrc.js to extend Prettier:

extends: [
  // ...existing extends
  "prettier" // Add this last to disable conflicting rules
]

Add format scripts:

"scripts": {
  "format": "prettier --write src/**/*.ts", // Auto-format files
  "format:check": "prettier --check src/**/*.ts" // Check for unformatted files
}

Testing with Vitest

Vitest is a fast, Vite-native test runner. For Vite projects, it’s pre-configured—just install:

npm install --save-dev vitest

Add a test script to package.json:

"scripts": {
  "test": "vitest"
}

Write a test in src/main.test.ts:

import { describe, it, expect } from "vitest";
import { greet } from "./main"; // Assume we exported `greet` from main.ts

describe("greet", () => {
  it("returns a greeting message", () => {
    expect(greet("Test")).toBe("Hello, Test!");
  });
});

Run tests:

npm test

6. Tsconfig.json Deep Dive

tsconfig.json controls TypeScript’s behavior. Here are critical options:

Key Configuration Options

OptionPurpose
targetJS version to compile to (e.g., ES2020, ESNext).
moduleModule system (CommonJS for Node.js, ESNext for browsers).
outDirWhere to output compiled JS (e.g., ./dist).
rootDirRoot folder for source TS files (e.g., ./src).
strictEnables all strict type-checking (always set to true).
esModuleInteropAllows import of CommonJS modules (e.g., import fs from 'fs').
moduleResolutionHow modules are resolved (Node for Node.js, Bundler for Vite/Rollup).

Strict Mode: Why It Matters

Strict mode enables rules like:

  • noImplicitAny: Disallows untyped variables (avoids any pitfalls).
  • strictNullChecks: Requires explicit handling of null/undefined.
  • strictFunctionTypes: Ensures function parameters are type-safe.

Never disable strict mode unless migrating a legacy JS project!

7. Advanced: Scaffolding Monorepos with Turborepo

For large projects (e.g., frontend + backend + shared libs), use a monorepo tool like Turborepo to manage multiple packages efficiently.

What is a Monorepo?

A monorepo houses multiple projects (apps/packages) in a single repo, enabling shared code, unified tooling, and coordinated versioning.

Scaffold with Turborepo

npx create-turbo@latest

Follow the prompts to create a monorepo with:

  • A packages/ folder for shared code (e.g., eslint-config, ui).
  • An apps/ folder for apps (e.g., web (Vite frontend), api (Node.js backend)).

Run Commands Across Projects

Turborepo runs commands in parallel and caches results for speed:

# Build all apps/packages
npm run build

# Run tests for all projects
npm run test

# Start dev servers for web and api
npm run dev

8. Conclusion

Scaffolding TypeScript projects has never been easier with modern tools like Vite, ts-node, and Turborepo. To recap:

  • For frontend apps: Use Vite for fast HMR and optimized builds.
  • For Node.js backends: Use ts-node + nodemon for live reloading.
  • For large projects: Use Turborepo to manage monorepos efficiently.
  • Always enable strict: true in tsconfig.json for type safety.
  • Integrate ESLint, Prettier, and Vitest/Jest for code quality and testing.

9. References