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
:
- Files: The collection of
.ts
(and potentially.js
or.tsx
) files that TypeScript will parse and analyze. - 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:
Property | Description | Details |
files | Specifies an explicit array of input files to include. | Not generally recommended because it requires constant manual updating. If specified, it overrides the exclude property. |
include | Specifies files, folders, or glob patterns to include in the compilation. | This is the most common and flexible method for defining input. |
exclude | Specifies 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 Examples | Description |
"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 Examples | Use 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.
- 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. - 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 (implyingany
forthis
). - 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.