TypeScript

TypeScript Compiler & Configuration

25 min Lesson 2 of 40

Understanding the TypeScript Compiler

The TypeScript compiler (tsc) is the tool that transforms your TypeScript code into JavaScript. Understanding how to configure and control the compiler is essential for building robust TypeScript applications. The compiler does much more than just remove type annotations—it can check for errors, transform modern syntax, optimize output, and enforce coding standards.

The tsconfig.json File

The tsconfig.json file is the heart of any TypeScript project. It tells the compiler how to process your code and what rules to enforce. When you run tsc without any arguments in a directory containing a tsconfig.json file, the compiler uses the settings from that file.

Creating a tsconfig.json

The easiest way to create a tsconfig.json file is to use the --init flag:

tsc --init

This creates a tsconfig.json file with many options commented out. Let's start with a minimal configuration and build from there:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

Important: The tsconfig.json file uses JSON format, which means no trailing commas and all keys must be in double quotes. Comments are supported using // or /* */ syntax, even though standard JSON doesn't allow comments.

Essential Compiler Options

Target and Module

These two options control what JavaScript version the compiler outputs:

target

Specifies which ECMAScript version to compile to. Common values:

"target": "ES5"      // For older browsers (IE11)
"target": "ES6"      // Also known as ES2015
"target": "ES2020"   // Modern JavaScript
"target": "ESNext"   // Latest features

Recommendation: Use "ES2020" or "ES2021" for modern applications. Use "ES5" only if you need to support very old browsers.

module

Specifies the module system for the output:

"module": "commonjs"   // Node.js style (require/module.exports)
"module": "es2015"     // Modern ES modules (import/export)
"module": "esnext"     // Latest module features
"module": "amd"        // For browser module loaders
"module": "umd"        // Universal module definition

Common Setup: For Node.js projects, use "commonjs". For modern frontend projects with bundlers like Webpack or Vite, use "esnext" or "es2015".

Output Control

outDir and rootDir

Control where compiled files are placed:

{
  "compilerOptions": {
    "rootDir": "./src",      // Where TypeScript files are located
    "outDir": "./dist"       // Where compiled JavaScript goes
  }
}

With this configuration, a file at src/utils/helper.ts compiles to dist/utils/helper.js, preserving the directory structure.

Other Output Options

{
  "compilerOptions": {
    "removeComments": true,           // Remove comments from output
    "sourceMap": true,                // Generate .map files for debugging
    "declaration": true,              // Generate .d.ts type definition files
    "declarationMap": true,           // Generate source maps for .d.ts files
    "inlineSourceMap": false,         // Include source maps inline
    "outFile": "./dist/bundle.js"     // Concatenate all output into one file
  }
}

Warning: The outFile option only works with "module": "amd" or "module": "system". For most modern projects, use a bundler like Webpack instead of outFile.

Module Resolution

{
  "compilerOptions": {
    "moduleResolution": "node",        // Use Node.js module resolution
    "baseUrl": "./",                   // Base directory for module resolution
    "paths": {                         // Path mapping for imports
      "@utils/*": ["src/utils/*"],
      "@models/*": ["src/models/*"],
      "@components/*": ["src/components/*"]
    },
    "esModuleInterop": true,          // Enable interop between CommonJS and ES modules
    "allowSyntheticDefaultImports": true,  // Allow default imports from modules with no default export
    "resolveJsonModule": true         // Allow importing .json files
  }
}

With path mapping configured, you can import like this:

// Instead of relative imports
import { formatDate } from '../../../utils/dateFormatter';

// Use clean path aliases
import { formatDate } from '@utils/dateFormatter';

Note: Path mapping in tsconfig.json is only for TypeScript compilation. If you use a bundler, you'll need to configure the same paths there (e.g., in Webpack's resolve.alias).

Strict Mode Options

TypeScript's strict mode enables all strict type-checking options. This is highly recommended for new projects:

{
  "compilerOptions": {
    "strict": true
  }
}

Setting "strict": true enables all of these options:

Individual Strict Options

strictNullChecks

When enabled, null and undefined are not in the type of every value:

// Without strictNullChecks
let name: string = null;  // OK

// With strictNullChecks
let name: string = null;  // Error: Type 'null' is not assignable to type 'string'
let name: string | null = null;  // OK - explicitly allow null

strictFunctionTypes

Enables stricter checking of function parameter types:

type Logger = (message: string | number) => void;

// Without strictFunctionTypes - OK
const numberLogger: Logger = (message: number) => {
  console.log(message);
};

// With strictFunctionTypes - Error
// Function parameters must be compatible

strictBindCallApply

Checks that bind, call, and apply methods are invoked with correct arguments:

function greet(name: string, age: number) {
  console.log(`Hello ${name}, you are ${age} years old`);
}

// With strictBindCallApply
greet.call(null, "Alice", 25);      // OK
greet.call(null, "Alice", "25");    // Error: Argument of type 'string' is not assignable to parameter of type 'number'
greet.apply(null, ["Alice"]);       // Error: Expected 2 arguments, but got 1

noImplicitAny

Raises an error when TypeScript can't infer a type and defaults to any:

// Without noImplicitAny - OK
function log(message) {  // message is implicitly 'any'
  console.log(message);
}

// With noImplicitAny - Error
function log(message) {  // Error: Parameter 'message' implicitly has an 'any' type
  console.log(message);
}

// Fixed
function log(message: string) {  // OK
  console.log(message);
}

noImplicitThis

Raises an error when this has an implicit any type:

// Without noImplicitThis
const obj = {
  name: "Alice",
  greet() {
    return function() {
      console.log(this.name);  // 'this' is implicitly 'any'
    };
  }
};

// With noImplicitThis - must specify this type
const obj = {
  name: "Alice",
  greet() {
    return function(this: { name: string }) {
      console.log(this.name);
    };
  }
};

alwaysStrict

Ensures all files are parsed in ECMAScript strict mode and emits "use strict" in each output file:

// Output JavaScript will include:
"use strict";

// This prevents common JavaScript pitfalls like:
x = 10;  // Error: Cannot assign to undeclared variable

Best Practice: Always enable strict mode for new projects. It catches many potential bugs and enforces better coding practices. For existing JavaScript projects being migrated to TypeScript, you might start with strict mode disabled and gradually enable strict options one at a time.

Other Important Options

Code Quality Options

{
  "compilerOptions": {
    "noUnusedLocals": true,           // Error on unused local variables
    "noUnusedParameters": true,       // Error on unused function parameters
    "noImplicitReturns": true,        // Error when not all code paths return a value
    "noFallthroughCasesInSwitch": true, // Error on fallthrough cases in switch
    "allowUnreachableCode": false,    // Error on unreachable code
    "allowUnusedLabels": false        // Error on unused labels
  }
}

Library and Type Definition Options

{
  "compilerOptions": {
    "lib": ["ES2020", "DOM"],         // Type definitions to include
    "types": ["node", "jest"],        // Specific @types packages to include
    "skipLibCheck": true              // Skip type checking of declaration files
  }
}

The lib option is crucial—it tells TypeScript what built-in APIs are available:

// For Node.js projects
"lib": ["ES2020"]

// For browser projects
"lib": ["ES2020", "DOM", "DOM.Iterable"]

// For projects that need both
"lib": ["ES2020", "DOM", "WebWorker"]

Experimental and Advanced Options

{
  "compilerOptions": {
    "experimentalDecorators": true,   // Enable experimental decorator syntax
    "emitDecoratorMetadata": true,    // Emit metadata for decorators
    "forceConsistentCasingInFileNames": true,  // Ensure consistent file name casing
    "isolatedModules": true,          // Ensure each file can be safely transpiled
    "jsx": "react",                   // JSX mode for React
    "incremental": true,              // Enable incremental compilation for faster builds
    "tsBuildInfoFile": "./dist/.tsbuildinfo"  // Where to store incremental build info
  }
}

Include and Exclude

Control which files the compiler processes:

{
  "include": [
    "src/**/*"          // Include all files in src and subdirectories
  ],
  "exclude": [
    "node_modules",     // Exclude dependencies
    "**/*.spec.ts",     // Exclude test files
    "**/*.test.ts",     // Exclude test files
    "dist"              // Exclude compiled output
  ]
}

Default Behavior: If you don't specify include, TypeScript includes all .ts, .tsx, and .d.ts files in the project. If you don't specify exclude, it automatically excludes node_modules, bower_components, and the output directory.

Complete Real-World Example

Here's a comprehensive tsconfig.json for a modern Node.js project:

{
  "compilerOptions": {
    // Language and Environment
    "target": "ES2020",
    "lib": ["ES2020"],
    "module": "commonjs",
    "moduleResolution": "node",

    // Output
    "rootDir": "./src",
    "outDir": "./dist",
    "removeComments": true,
    "sourceMap": true,
    "declaration": true,

    // Type Checking
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,

    // Module Resolution
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "forceConsistentCasingInFileNames": true,

    // Performance
    "skipLibCheck": true,
    "incremental": true,

    // Path Mapping
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"],
      "@utils/*": ["src/utils/*"],
      "@models/*": ["src/models/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

Compiler CLI Usage

Basic Commands

// Compile with tsconfig.json
tsc

// Compile specific file
tsc file.ts

// Compile with custom config
tsc --project tsconfig.production.json

// Watch mode
tsc --watch

// Show compiler version
tsc --version

// Show help
tsc --help

Overriding Config Options

You can override tsconfig.json options from the command line:

// Override target
tsc --target ES5

// Override outDir
tsc --outDir ./build

// Enable source maps
tsc --sourceMap true

// Compile despite errors
tsc --noEmitOnError false

Multiple Configuration Files

For complex projects, you might have different configurations for different environments:

// tsconfig.json (base configuration)
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2020"
  }
}

// tsconfig.dev.json (development)
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "sourceMap": true,
    "incremental": true
  }
}

// tsconfig.prod.json (production)
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "removeComments": true,
    "sourceMap": false
  }
}

Use them with:

tsc --project tsconfig.dev.json
tsc --project tsconfig.prod.json

Practice Exercise

Create a TypeScript project with proper configuration:

  1. Create a new directory called typescript-config-practice
  2. Initialize npm: npm init -y
  3. Install TypeScript: npm install --save-dev typescript
  4. Create a tsconfig.json with:
    • Strict mode enabled
    • ES2020 target
    • Source maps enabled
    • src/ as root directory
    • dist/ as output directory
  5. Create src/index.ts with:
    interface User {
      id: number;
      name: string;
      email: string;
    }
    
    function getUser(id: number): User {
      return {
        id,
        name: "John Doe",
        email: "john@example.com"
      };
    }
    
    const user = getUser(1);
    console.log(`User: ${user.name} (${user.email})`);
  6. Compile using npx tsc
  7. Run the compiled code: node dist/index.js
  8. Try removing the email property from the returned object and see what error TypeScript gives
  9. Enable watch mode and make changes to see automatic recompilation

Summary

  • The tsconfig.json file controls how TypeScript compiles your code
  • target and module control the JavaScript version and module system
  • outDir and rootDir control file organization
  • "strict": true enables all strict type-checking options and is highly recommended
  • Path mapping allows clean imports without complex relative paths
  • The compiler can generate source maps, declaration files, and optimize output
  • You can have multiple config files for different environments using extends
  • The TypeScript compiler CLI provides many options for compilation and error checking

Next Steps: Now that you understand how to configure TypeScript, we'll explore the type system in detail, starting with basic types in the next lesson.