javascriptroom guide

Anatomy of a TypeScript Project: Folder Structure and Configuration

TypeScript has emerged as a cornerstone of modern JavaScript development, offering type safety, enhanced tooling, and improved scalability over vanilla JavaScript. Whether you’re building a small library, a backend API, or a large frontend application, a well-organized project structure and thoughtful configuration are critical for maintainability, collaboration, and long-term success. A disorganized project with haphazardly placed files and unconfigured tools can lead to confusion, bugs, and wasted time. Conversely, a clean structure and optimized configuration streamline onboarding, reduce errors, and make scaling your project a breeze. In this blog, we’ll dissect the anatomy of a TypeScript project, exploring **folder structure** (from basic to advanced setups) and **key configuration files** (like `tsconfig.json`, ESLint, and build tools). By the end, you’ll have a blueprint to organize your TypeScript projects like a pro.

Table of Contents

  1. Folder Structure
  2. Configuration Files
  3. Best Practices
  4. Conclusion
  5. References

Folder Structure

A project’s folder structure is its skeleton—it defines where code, assets, tests, and configs live. The right structure depends on your project’s size (small library vs. enterprise app) and type (frontend, backend, CLI tool). Let’s start with the basics.

Basic Project Structure

For small projects (e.g., a utility library, simple Node.js script, or prototype), a minimal structure suffices. Here’s a typical setup:

my-typescript-project/  
├── src/                  # Source code (TypeScript)  
│   ├── index.ts          # Entry point  
│   └── utils/            # Helper functions  
│       └── format.ts  
├── dist/                 # Compiled JavaScript (output)  
├── tests/                # Test files  
│   ├── unit/             # Unit tests  
│   └── integration/      # Integration tests  
├── package.json          # Dependencies and scripts  
├── tsconfig.json         # TypeScript compiler config  
├── .gitignore            # Files to ignore (node_modules, dist, etc.)  
└── README.md             # Project documentation  

Key Folders Explained:

  • src/: Contains all TypeScript source code. Keep this clean—only write code here that needs compilation.
  • dist/: Output directory for compiled JavaScript (generated by tsc). Add this to .gitignore to avoid committing build artifacts.
  • tests/: Organize tests by type (unit, integration, E2E). Tools like Jest or Vitest will run these.

Advanced Project Structure

As projects grow (e.g., a React app, enterprise backend, or monorepo), you’ll need a more granular structure to enforce separation of concerns. Here’s an expanded example:

my-advanced-ts-project/  
├── src/  
│   ├── index.ts                  # Entry point  
│   ├── api/                      # API clients (e.g., fetch wrappers)  
│   │   └── github-api.ts  
│   ├── components/               # Reusable UI components (frontend)  
│   │   ├── Button/  
│   │   │   ├── Button.tsx  
│   │   │   └── Button.test.tsx  
│   ├── hooks/                    # Custom React hooks (frontend)  
│   │   └── useLocalStorage.ts  
│   ├── models/                   # TypeScript interfaces/types  
│   │   └── User.ts  
│   ├── services/                 # Business logic (e.g., auth, data processing)  
│   │   └── user-service.ts  
│   └── utils/                    # Shared utilities (e.g., validation, formatting)  
├── public/                       # Static assets (images, CSS, HTML) [frontend]  
├── config/                       # Shared configuration files  
│   ├── eslint.base.js            # Base ESLint rules  
│   └── jest.base.js              # Base Jest config  
├── scripts/                      # Build/deployment scripts (e.g., deploy.sh)  
├── docs/                         # Documentation (API docs, guides)  
├── .env.example                  # Example environment variables  
└── (other root files: tsconfig.json, package.json, etc.)  

New Additions:

  • src/models/: Centralizes TypeScript interfaces and types (e.g., User, Product). This avoids duplicating types across files.
  • src/services/: Encapsulates business logic (e.g., API calls, data transformation) to keep components/hooks lean.
  • public/: For static assets (images, fonts) that don’t need compilation (common in frontend frameworks like React/Vue).
  • config/: Stores shared configs (e.g., ESLint, Jest) to reuse across subprojects (useful in monorepos).

Project-Specific Variations

Folder structure varies by project type. Here are examples for common use cases:

Frontend (React/Vue/Svelte):

  • Add src/pages/ for route-specific components (e.g., HomePage.tsx, AboutPage.tsx).
  • src/context/ for React Context providers (state management).
  • src/assets/ for images, fonts, or SCSS files (if not using public/).

Backend (Node.js/Express/NestJS):

  • src/controllers/ for request handlers (e.g., UserController.ts).
  • src/routes/ for API route definitions (e.g., user.routes.ts).
  • src/middleware/ for Express middleware (e.g., auth, logging).
  • src/db/ for database models/migrations (e.g., Prisma schema, Sequelize models).

CLI Tool:

  • src/commands/ for CLI command handlers (e.g., init.ts, deploy.ts).
  • src/cli.ts as the entry point for parsing CLI arguments (using commander or yargs).

Configuration Files

Configuration files dictate how tools (TypeScript, linters, bundlers) behave. Let’s break down the most critical ones.

tsconfig.json: The Heart of TypeScript

The tsconfig.json file tells the TypeScript compiler (tsc) how to transpile TypeScript to JavaScript. It’s required for any TypeScript project. Here’s a typical setup with key options explained:

// tsconfig.json  
{  
  "compilerOptions": {  
    // Output settings  
    "target": "ES2020",          // Compile to ES2020 (supports modern JS features)  
    "module": "ESNext",          // Use ES modules (import/export)  
    "outDir": "./dist",          // Output compiled JS to `dist/`  
    "rootDir": "./src",          // Source code lives in `src/`  
    "declaration": true,         // Generate .d.ts type files (for libraries)  
    "sourceMap": true,           // Generate source maps for debugging  

    // Type-checking strictness  
    "strict": true,              // Enable all strict type-checking (critical!)  
    "noImplicitAny": true,       // Disallow `any` types (unless explicitly used)  
    "strictNullChecks": true,    // Require null/undefined checks (avoids "cannot read property of undefined")  

    // Module resolution  
    "moduleResolution": "NodeNext", // Use Node.js module resolution (supports package.json "exports")  
    "esModuleInterop": true,     // Enable interop between CommonJS and ES modules  
    "skipLibCheck": true,        // Skip type-checking for .d.ts files (faster builds)  
    "forceConsistentCasingInFileNames": true, // Avoid OS-specific casing issues  

    // Libraries to include (e.g., DOM for frontend, ES2020 for backend)  
    "lib": ["ES2020", "DOM"]     // "DOM" is optional (only for frontend)  
  },  
  "include": ["src/**/*"],       // Files to compile (all .ts/.tsx in src/)  
  "exclude": ["node_modules", "dist", "tests/**/*.test.ts"] // Files to ignore  
}  

Pro Tip: Use extends for Reusability

For large projects or monorepos, split tsconfig.json into base and project-specific configs:

// tsconfig.base.json (shared settings)  
{  
  "compilerOptions": {  
    "strict": true,  
    "target": "ES2020",  
    // ... other shared options  
  }  
}  

// tsconfig.app.json (for the app)  
{  
  "extends": "./tsconfig.base.json",  
  "compilerOptions": {  
    "outDir": "./dist/app"  
  },  
  "include": ["src/app/**/*"]  
}  

Popular base configs: @tsconfig/node18, @tsconfig/react-app.

Linting & Formatting: ESLint and Prettier

TypeScript enforces type safety, but ESLint and Prettier enforce code quality and style.

ESLint (Linting)

ESLint catches code errors, anti-patterns, and enforces coding standards (e.g., no console.log in production). For TypeScript, use @typescript-eslint:

  1. Install dependencies:

    npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev  
  2. Create .eslintrc.js:

    // .eslintrc.js  
    module.exports = {  
      parser: '@typescript-eslint/parser', // Parse TypeScript  
      parserOptions: {  
        project: './tsconfig.json', // Enable type-aware linting  
        tsconfigRootDir: __dirname,  
      },  
      extends: [  
        'eslint:recommended',  
        'plugin:@typescript-eslint/recommended', // TypeScript rules  
        'plugin:@typescript-eslint/strict-type-checked', // Strict type checks  
      ],  
      rules: {  
        // Custom rules (e.g., disallow console.log)  
        'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',  
        '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],  
      },  
    };  

Prettier (Formatting)

Prettier auto-formats code (e.g., line length, indentation) to avoid style debates. Integrate it with ESLint using eslint-config-prettier (disables ESLint rules that conflict with Prettier):

  1. Install dependencies:

    npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev  
  2. Create .prettierrc:

    // .prettierrc  
    {  
      "semi": true,  
      "singleQuote": true,  
      "tabWidth": 2,  
      "trailingComma": "es5"  
    }  
  3. Update ESLint to use Prettier:

    // .eslintrc.js (updated)  
    module.exports = {  
      // ... (previous config)  
      extends: [  
        // ... (previous extends)  
        'plugin:prettier/recommended', // Enables Prettier as an ESLint rule  
      ],  
    };  

Build Tools: Webpack, Vite, and Rollup

For frontend projects or libraries, you’ll need a bundler to package code. Popular tools:

Vite (Fast Frontend Bundler)

Vite is faster than Webpack and natively supports TypeScript. Configure it with vite.config.ts:

// vite.config.ts  
import { defineConfig } from 'vite';  
import react from '@vitejs/plugin-react'; // For React  

export default defineConfig({  
  plugins: [react()],  
  build: {  
    outDir: 'dist', // Output directory  
  },  
  server: {  
    port: 3000, // Dev server port  
  },  
});  

Rollup (Library Bundler)

For TypeScript libraries, Rollup creates optimized bundles (ESM, CommonJS). Use @rollup/plugin-typescript:

// rollup.config.js  
import typescript from '@rollup/plugin-typescript';  

export default {  
  input: 'src/index.ts',  
  output: [  
    { file: 'dist/index.cjs', format: 'cjs' }, // CommonJS  
    { file: 'dist/index.mjs', format: 'esm' }, // ESM  
  ],  
  plugins: [typescript()],  
};  

Testing: Jest Configuration

Jest is a popular testing framework for TypeScript. Use ts-jest to transpile TypeScript during tests:

  1. Install dependencies:

    npm install jest ts-jest @types/jest --save-dev  
  2. Create jest.config.js:

    // jest.config.js  
    module.exports = {  
      preset: 'ts-jest', // Use ts-jest for TypeScript  
      testEnvironment: 'node', // or 'jsdom' for frontend  
      roots: ['<rootDir>/src'], // Look for tests in src/  
      testMatch: ['**/*.test.ts'], // Test file pattern  
      moduleFileExtensions: ['ts', 'js'],  
    };  

package.json Scripts

package.json scripts automate common tasks (build, test, lint). Here are essential scripts:

// package.json  
{  
  "scripts": {  
    "build": "tsc", // Compile TypeScript to JS  
    "dev": "ts-node src/index.ts", // Run with ts-node (no build step)  
    "watch": "tsc --watch", // Recompile on file changes  
    "test": "jest", // Run tests  
    "test:watch": "jest --watch", // Run tests in watch mode  
    "lint": "eslint 'src/**/*.{ts,tsx}'", // Lint code  
    "format": "prettier --write 'src/**/*.{ts,tsx,json,md}'", // Format code  
    "prepare": "husky install" // Set up Husky (optional, for pre-commit hooks)  
  }  
}  

Best Practices

  1. Separate Source and Output: Keep src/ (TypeScript) and dist/ (compiled JS) strictly separated. Add dist/ to .gitignore.
  2. Centralize Types: Store interfaces/types in src/models/ to avoid duplication.
  3. Keep Configs DRY: Use extends in tsconfig.json, ESLint, and Jest to reuse configs across projects.
  4. Modularize Code: Split large files into smaller, single-responsibility modules (e.g., one utility per file).
  5. Document Structure: Add a README.md explaining folder purposes (e.g., “src/services/ holds business logic”).
  6. Ignore Unnecessary Files: Use .gitignore to exclude node_modules/, dist/, .env, and IDE files (.vscode/).

Conclusion

A well-structured TypeScript project with clear configuration is the foundation of scalability and collaboration. By organizing folders logically, leveraging tools like tsconfig.json and ESLint, and automating tasks with scripts, you’ll reduce errors, speed up development, and make onboarding new team members a breeze.

Remember: There’s no “one-size-fits-all”—adapt the structure and configs to your project’s needs, but start with these principles to avoid common pitfalls.

References