Page Content

Tutorials

What is TypeScript Compilation? Explained With Code

TypeScript Compilation

The logical grouping of files that TypeScript processes and analyses is referred to as the Compilation Context, which is essentially an elegant word. It functions as the framework that the TypeScript compiler (tsc) uses to identify the existence of errors and the validity of types.

The TypeScript compiler internally uses a Program object to specify the compilation context. There are two main parts to this program:

  1. Files: The collection of .ts (and potentially .js or .tsx) files that TypeScript will parse and analyze.
  2. Compiler Options: The configuration settings that dictate how TypeScript should process and compile these files.

A fundamental and recommended way to define this logical grouping or “project” is by using a tsconfig.json file.

Understanding

When a directory has a tsconfig.json file, it indicates that the directory is the root of a project that uses TypeScript. The root files and all the compiler options required to build the project are specified in this configuration file.

Defining Input Files

The files that are included in the compilation context are managed by the tsconfig.json file. The compiler will look for a tsconfig.json file starting in the current directory and working up the parent chain if you execute the tsc command without providing any input files.

Basic Inclusion Rules

By default, if the files property is omitted, the compiler includes all TypeScript (*.ts or *.tsx) files within the containing directory and all subdirectories. Unless specified otherwise, directories like node_modules are automatically excluded from the compilation process.

Explicit File Control

Several properties can be used to control which files are included and which are excluded:

PropertyDescriptionDetails
filesSpecifies an explicit array of input files to include.Not generally recommended because it requires constant manual updating. If specified, it overrides the exclude property.
includeSpecifies files, folders, or glob patterns to include in the compilation.This is the most common and flexible method for defining input.
excludeSpecifies files or folders to omit from the set defined by include.By default, this includes ["node_modules", "bower_components", "jspm_packages"] to avoid processing third-party libraries.

Glob Example The pattern **/* can be used in include to mean all folders and any files, automatically assuming .ts or .tsx extensions, or .js/.jsx if allowJs:true is set.

Initializing the Project

The TypeScript compiler initialisation command can be used to quickly set up a tsconfig.json file:

tsc --init

This program creates a configuration file with many commented-out options and default settings, which is a useful place to start.

As of TypeScript v2.3.0 and beyond, a standard default tsconfig.json produced by tsc --init has the following structure, establishing default settings for essential options:

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5", 
    "module": "commonjs", 
    // "lib": [], // Specify library files to be included
    // "outDir": "./", // Redirect output structure
    // "rootDir": "./", // Specify the root directory of input files

    /* Strict Type-Checking Options */
    "strict": true,
    // "noImplicitAny": true, // Raise error on expressions with implied 'any'
    // "strictNullChecks": true, // Enable strict null checks
    // ... many more options ...
  }
}

Once a tsconfig.json is in place, simply running tsc in the terminal will use this configuration to compile all corresponding TypeScript files into JavaScript, generally creating .js files automatically.

Essential Compiler Options

The core configuration settings are placed inside the compilerOptions property within the tsconfig.json file.

ECMAScript Target Version

The ECMAScript version to which TypeScript should compile the code is specified by the target parameter. This establishes which features of the more recent JavaScript syntax must be translated to the more archaic, comparable syntax.

Value ExamplesDescription
"es5", "es2015"Common targets for broader compatibility.
"esnext"Targets the latest features, meaning virtually nothing is transpiled.

For example, compiling ES2015 const declarations and nullish coalescing ?? syntax with a --target es5 requires conversion to var and older logical expressions for compatibility.

Module Code Generation

The module system utilised in the JavaScript output that is emitted is determined by the module option. Compatibility with bundlers (like Webpack) or the runtime environment (like Node.js) depends on this.

Value ExamplesUse Case
"commonjs"Standard for Node.js environments.
"amd", "system", "umd"Used for specific module loaders or libraries.
"es2015" / "esnext"Often preferred when using modern bundlers like Webpack or Rollup.

For server-side NodeJS environments, setting "module": "commonjs" is recommended, which compiles ES2015 import/export syntax into require/module.exports calls.

Library Files Inclusion

The lib option specifies the library definition files (lib.*.d.ts) that define the set of global APIs TypeScript should assume exist in the target runtime environment. Using --lib provides finer-grained control and decouples the available ambient library support from the chosen --target.

The libraries that are offered are divided into categories, such as:

  • JavaScript Bulk Features: e.g., es5, es6, es2015, esnext.
  • Runtime Environments: e.g., dom, webworker, scripthost.

A default library set is injected depending on the target if lib is not specifically provided. For example, a typical suggestion is:

"compilerOptions": {
  "target": "es5",
  "lib": ["es6", "dom"]
}

Targeting ES5 output is made possible by this configuration, which also acknowledges the existence of contemporary features like Promise (part of the es6 lib) and browser APIs (part of the dom lib) for type checking.

Project Structure Control

These settings aid in project structure management by specifying the location of input files and the location of output files.

  1. rootDir: Specifies the root directory containing the input files. TypeScript typically calculates the root based on the longest common sub-path of all input files, but explicitly setting rootDir helps control the output structure.
  2. outDir: Specifies the directory where the compiled JavaScript output should be placed. The output file structure mirrors the relative path of the files from rootDir.

For example, if you set rootDir: "src" and outDir: "dist", an input file src/utils/file.ts will be compiled to dist/utils/file.js.

Setting Strict Checks for Type Safety

Through a variety of compiler parameters, TypeScript allows developers to regulate the “soundness” of the type system, allowing them to decide how rigorously the code should be examined. A number of “strict” tests need to be enabled in order to maximise safety and take full advantage of static analysis.

True

Setting the strict flag to true is the simplest approach to enable a high level of type safety. All severe type-checking features are enabled concurrently by this single flag.

The following significant flags are usually set to true (among others) when strict: true is enabled:

  • noImplicitAny: Prevents implied any types.
  • strictNullChecks: Prevents silent use of null/undefined.
  • noImplicitThis: Raises an error if the type of this cannot be inferred (implying any for this).
  • alwaysStrict: Parses code in strict mode and emits "use strict" for each file.
  • strictFunctionTypes: Enables stronger checks for function type compatibility (contravariance in parameters).
  • strictPropertyInitialization: Ensures class properties are initialized unless explicitly marked as nullable.

Any

When TypeScript is unable to infer a type and sets the variable, parameter, or expression type to any, this option generates an error.

Runtime issues may arise because the any type functions as a wildcard, enabling its value to get around the majority of the type checker. You can either depend on good type inference in every location or impose explicit type annotations by setting "noImplicitAny": true.

Code Example: noImplicitAny

Consider the following file, implicitTest.ts, without any type annotation on the function parameter:

// implicitTest.ts
function processData(data) {
    return data.toUpperCase();
}
processData("hello");

If noImplicitAny: true is set (either directly or via strict: true), compiling this file (tsc implicitTest.ts) results in a type error because TypeScript cannot infer the type of data:

// Output (Compiler Error):
error TS7006: Parameter 'data' implicitly has an 'any' type.

To resolve this error, you must explicitly annotate the parameter’s type:

function processData(data: string) {
    return data.toUpperCase(); // OK
}

Null and Undefined

Strict checks for null and undefined values are made possible by the strictNullChecks option. JavaScript has historically permitted null in all types’ domains, which results in the “billion dollar mistake” unexpected runtime problems when accessing properties of null.

When strictNullChecks: true is active, null and undefined are treated as separate types and are only assignable to themselves, to any, or to types that explicitly include them via a union (e.g., string | null or number | undefined). This prevents functions expecting a value from being passed null implicitly.

Code Example: strictNullChecks

Consider the file nullTest.ts with strict checks enabled:

// nullTest.ts
function getLength(input: string): number {
    return input.length;
}

// 1. Passing null where string is expected
getLength(null);

// 2. Variable that might be null
let maybeValue: string | null = "test";
maybeValue = null;

// Accessing property on potential null value
console.log(maybeValue.length);

Compiling this file (tsc nullTest.ts) with strictNullChecks: true will raise errors for both issues:

// Output (Compiler Errors):
nullTest.ts:6:11 - error TS2345: Argument of type 'null' is not assignable to parameter of type 'string'.
6 getLength(null);
           ~~~~

nullTest.ts:12:21 - error TS2531: Object is possibly 'null'.
12 console.log(maybeValue.length);

In order to resolve these problems and avoid runtime exceptions, the programmer must use non-null assertions or runtime checks (such as type guards) to ensure that the value is not null before accessing its properties.

In Conclusion

The Compilation Context provides the necessary boundaries for TypeScript’s analysis, primarily defined by the tsconfig.json file. This file manages which files are included and controls the compiler’s behaviour through options like target, module, rootDir, outDir, and lib. By enabling strict: true, along with specific checks like noImplicitAny and strictNullChecks, you ensure the highest level of type safety, allowing TypeScript to catch many common errors statically, long before the code is executed.





Index