Understanding TypeScript's Compilation Process & the anatomy of tsconfig.json file to configure TypeScript Compiler

In this lesson, we are going to learn about the settings of the TypeScript compiler and the usage of the tsconfig.json file.

TypeScript provides a command-line utility tsc that compiles (transpiles) TypeScript files (.ts) into JavaScript. However, the tsc compiler (short for TypeScript compiler) needs a JSON configuration file to look for TypeScript files in the project and generate valid output files at a correct location.

When you run tsc command in a directory, TypeScript compiler looks for the tsconfig.json file in the current directory and if it doesn’t find one, then it keeps looking up the directory tree until it finds one. The directory where the tsconfig.json is located is considered as the root of the project.

You can manually provide a path to the tsconfig.json file using --project or -p command-line flag. This file doesn’t need to have the tsconfig.json filename if you are using this flag with the exact file path. However, you can also provide the directory path that contains the tsconfig.json file.

$ tsc -p /proj/x/tsconfig.dev.json

If the TypeScript compiler fails to locate this configuration file, you would get an error. But you can provide settings enlisted in this file through the equivalent command-line options which we will cover in the next lesson.

Structure of tsconfig.json

So what does this file contain and what exactly it controls?

{
  "files": ["src/lib/person.ts", "src/lib/student.ts", "src/main.ts"],
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "outDir": "./dist/development"
  }
}

The tsconfig.json file is a standard JSON file, however, it supports JSON5 specifications, so you can use comments, single quotes, and more. It contains some root-level options and some compiler options. The root-level options are options that are outside of the compilerOptions object, so in the above example, files is a root-level option.

The root-level options control how the project is presented to the TypeScript compiler, such as which TypeScript files to consider for the compilation. The compiler options contain settings for the TypeScript compiler such as where to output the compiled JavaScript files in the project directory.

ROOT-LEVEL OPTIONS

These options control how the project is presented to the TypeScript compiler for the compilation and static type analysis. These options must be kept outside compilerOptions object of the tsconfig.json file.

files

The files array contains the location of the TypeScript files to consider for the compilation. These can be either relative paths or absolute paths on the disk. A relative path is located relative to the location of the tsconfig.json file (AKA root of the project).

/projects/sample/
├── a.ts
├── src/
|  ├── b.ts
|  ├── c.ts
|  ├── ignore.ts
|  └── lib/
|     ├── d.ts
|     └── e.ts
└── tsconfig.json

Let’s consider that we have the above directory structure in our project. As you can see, the TypeScript files (.ts) are located in multiple directories. We want to compile all the .ts files except the ignore.ts file. Hence we would provide relative paths of these files in the files options of tsconfig.json.

// tsconfig.json
{
  "files": ["a.ts", "src/b.ts", "./src/c.ts", "src/lib/d.ts", "./src/lib/e.ts"]
}

You can also provide absolute paths of these files but relative paths are most recommended since they would be consistent on all the systems. All the relative paths are resolved against the path of tsconfig.json file in the project. You can optionally provide ./ or ../ prefix to locate the file.

Since we haven’t provided any compilerOptions values, all the default values for the compiler options are used which we will talk about in a bit. The TypeScript compiler compiles these files and outputs the JavaScript with .js extension by keeping the same file name as the individual input file.

The TypeScript compiler also preserves the original file path, hence the .js output file will be generated where the input file was in the directory structure. When you run the tsc command from the directory where your tsconfig.json file is located, you are going to see the result below.

/projects/sample/
├── a.js
├── a.ts
├── src/
|  ├── b.js
|  ├── b.ts
|  ├── c.js
|  ├── c.ts
|  ├── ignore.ts
|  └── lib/
|     ├── d.js
|     ├── d.ts
|     ├── e.js
|     └── e.ts
└── tsconfig.json

As you can see, the TypeScript compiler compiled all the input TypeScript files listed inside files array of tsconfig.json. You can’t see the ignore.js file since ignore.ts file was not included in the files array.

The directory where the tsconfig.json file is located is considered as the root of the project, AKA the root directory. You can also include a file from outside this root directory, such by including "../x.ts" in the files array where x would be in the parent directory of the root directory. Since the TypeScript compiler preserves the input file path, it will generate x.js in the parent directory of the root directory.

include & exclude

The files option is great when you have relatively few files to work with. But when your project is big and contains hundreds of source files located in a nested directory structure, then handpicking file paths is not practical.

To solve this issue, we can use include option. This option is just like files, however, we can optionally provide glob patterns to locate input files. The exclude options behave the same, except it removes the files from the compilation that may have been included by the include option.

// tsconfig.json
{
  "include": ["a.ts", "src/**/*.ts"],
  "exclude": ["./**/*/ignore.ts"]
}

In the above tsconfig.json, we have removed the files option and added include which adds a.ts file from the root directory and all the .ts file from the src directory. Since this would also include any ignore.ts from the src directory, we have provided the exclude option that excludes any ignore.ts file from the compilation if located inside the src directory.

When we run the tsc command now, results won’t be any different since the files considered for the compilation both in the previous example and this example are the same.

/projects/sample/
├── a.js
├── a.ts
├── src/
|  ├── b.js
|  ├── b.ts
|  ├── c.js
|  ├── c.ts
|  ├── ignore.ts
|  └── lib/
|     ├── d.js
|     ├── d.ts
|     ├── e.js
|     └── e.ts
└── tsconfig.json

The TypeScript compiler automatically excludes files from the "node_modules", "bower_components", "jspm_packages" and "<outDir>" directories, where <outDir> is the value of outDir compiler-option provided by you. This prevents any .ts file from these directories getting included in the compilation process by accident.

More on file inclusion and exclusion

Normally, you would use files or include option and provide file paths or glob patterns to include TypeScript files in the compilation process. However, you can drop the .ts extension from the glob pattern and let the TypeScript compiler search for the input files.

When a glob pattern doesn’t have a file extension, TypeScript looks for the files ending with .ts or .d.ts extension in the directory pointed by the glob pattern. The .d.ts file is a TypeScript declaration file, we have discussed this file type in the Declaration Files lesson (coming soon). When the allowJS compiler-option is set to true, the TypeScript will also search for .js files as well.

// tsconfig.json

{
  "include": ["a.ts", "src/**/*"],
  "exclude": ["./**/*/ignore.ts"]
}

In the above example, we have dropped the .ts extension from the glob pattern src/**/* which implicitly represents src/**/*.ts and src/**/*.d.ts (_also *src/**/*.js* if allowJS is _true).

💡 You can also provide a directory path instead of a glob pattern. When a directory path is provided, for example ./src/, the TypeScript compiler will treat it as ./src/**/* which will behave exactly like the above example.

The exclude option also treats glob patterns and directory paths in a similar fashion. Since the TypeScript compiler automatically excludes files from the "node_modules", "bower_components", "jspm_packages" and "<outDir>" directories, you would need to manually include files from these directories if you need to compile them as well but try to avoid it.

The files or include option specifies the files that must be included in the compilation process. However, you could be importing files from within a TypeScript file using import statements that are not included in the files or include option. This is most likely the case when you have a single file as the entry point of the application.

In that case, the TypeScript compiler would not complain and gladly compile them as well. Such imports would not get excluded despite the exclude option excludes them. Let’s see an example.

/projects/sample/
├── a.ts
├── src/
|  ├── b.ts
|  ├── c.ts    ↴(imports ./ignore.ts)
|  ├── ignore.ts
|  └── lib/
|     ├── d.ts
|     └── e.ts
└── tsconfig.json
// tsconfig.json
{
  "include": ["a.ts", "src/**/*"],
  "exclude": ["./src/**/ignore.ts"]
}

In the above tsconfig.json file, we are including all the .ts or .d.ts files from the src directory except the ignore.ts files. But since we have imported the ignore.ts file inside src/c.ts , the tsc command would also compile the ignore.ts file and we would see the output as below.

/projects/sample/
├── a.js
├── a.ts
├── src/
|  ├── b.js
|  ├── b.ts
|  ├── c.js
|  ├── c.ts
|  ├── ignore.ts
|  ├── ignore.js  ⫷
|  └── lib/
|     ├── d.js
|     ├── d.ts
|     ├── e.js
|     └── e.ts
└── tsconfig.json

As you can see, now the output shows ignore.js file since it compiled from ignore.ts by the TypeScript compiler as it was imported explicitly by the c.ts file.

COMPILER OPTIONS

Options that change the behavior of the TypeScript compiler are presented inside compilerOptions of the tsconfig.json. These are called compiler options and there are plenty of them, so let’s understand each at a time.

Output Settings

These compiler options control how the TypeScript compiler outputs compiled JavaScript files from the source files. These are the basic options to control the location of output files and how the compiled JavaScript code looks.

outDir

The outDir option specifies the directory for the output files generated by the TypeScript compiler. In the earlier example, we saw that the TypeScript compiler generated the .js file where its original source file was. This is because the TypeScript compiler remembers the source file path.

Using the outDir option, we can store these generated files in a separate location such that our source code does not get littered with output files.

/projects/sample/
├── dist/   ⫷
├── a.ts
├── src/
|  ├── b.ts
|  ├── c.ts
|  └── lib/
|     ├── d.ts
|     └── e.ts
└── tsconfig.json

We still have a relatively simple project setup but this time, we want to collect all output files inside dist/ directory. Hence the tsconfig.json file has outDir compiler-option that points to this path.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist" // ⫷ relative to this file
    }
}

Once we run tsc command, the dist folder will get created if it doesn’t exist and the TypeScript compiler uses this folder to place all output files.

/projects/sample/
├── dist/
|  ├── a.js
|  └── src/
|     ├── b.js
|     ├── c.js
|     └── lib/
|        ├── d.js
|        └── e.js
├── a.ts
├── src/...
└── tsconfig.json

As you can see from the above result, all the output .js files are now generated inside dist directory. The TypeScript compiler preserves the original source file path hence you get the above structure.

● rootDir

In the previous example, we saw how the TypeScript compiler remembers the path of a source file and uses it to place the output file at the current location in the directory structure. Let’s understand more about it.

When multiple files are part of the compilation process included by the files or the include option or by the import statements, their longest common path is calculated. Let’s evaluate the file paths of the input files.

/projects/sample/
├── a.ts (/projects/sample/a.ts)
├── src/
|  ├── b.ts (/projects/sample/src/b.ts)
|  ├── c.ts (/projects/sample/src/c.ts)
|  └── lib/
|     ├── d.ts (/projects/sample/src/lib/d.ts)
|     └── e.ts (/projects/sample/src/lib/e.ts)
└── tsconfig.json

As you can see, the longest common path between all the source files is /projects/sample. This longest common path is the value for the rootDir option. The path is can be provided relative to the tsconfig.json file as well, so it will be ./ (_or just _*'.'*) for the above example.

💡 If only the files from the src directory are included in the compilation, the longest common path would be /projects/sample/src or the relative common path (to the tsconfig.json) would be ./src.

When a value for this rootDir is explicitly provided, it should be a valid path that is common amongst all included files. The TypeScript compiler uses this common path to place output files in the output directory.

When the value of the outDir is not provided, the rootDir value is the default value for the outDir compiler-option. When the TypeScript compiler places an output file, it strips the rootDir value from the output file path and places the file inside outDir.

// tsconfig.json

{
  "include": ["a.ts", "src/**/*"]
}

For the above tsconfig.json file, since rootDir value is not explicitly provided, it will be /projects/sample/ if we look at the included files. Since the outDir value is also missing, it would be /projects/sample/.

When the TypeScript compiler compiles a.ts, the expected output file path is /projects/sample/a.js. But first, it will strip the rootDir path from it, hence the output path would be a.js and it would then place inside outDir which is /projects/sample/. Therefore, you will be able to find this file at /projects/sample/a.js location.

// tsconfig.json

{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist"
    }
}

In the above example, the outDir is set to dist which means all output files would be placed inside /projects/samples/dist/ directory. We haven’t provided the value for the rootDir option, so the output file path would be the same as earlier. With this, you would get the following results.

/projects/sample/
├── dist/
|  ├── a.js
|  └── src/
|     ├── b.js
|     ├── c.js
|     └── lib/
|        ├── d.js
|        └── e.js
├── a.ts
├── src/...
└── tsconfig.json

Let’s set the rootDir value to .. which makes /projects/ as the absolute rootDir path. With this change, the output file path for the a.ts input file would be sample/a.js since only the /projects/ part would be stripped out from the output file path.

// tsconfig.json

{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "rootDir": ".."
    }
}

When we run the tsc command again, we see the following output. Since sample/a.js is the final output path and the outDir is set to dist, you will find the a.js at the dist/sample/a.js location.

/projects/sample/
├── dist
|  └── sample/
|     ├── a.js
|     └── src
|        ├── b.js
|        ├── c.js
|        └── lib
|           ├── d.js
|           └── e.js
├── a.ts
├── src/...
└── tsconfig.json

You must remember to provide a valid rootDir which encapsulates all the input files. If a file does not belong to the rootDir, the TypeScript compiler throws an error. For example, if we set rootDir to ./src which doesn’t include a.ts, then you would get the following error.

🔥 Error TS6059: File '/projects/sample/a.ts' is not under 'rootDir' '/projects/sample/src'. 'rootDir' is expected to contain all source files.

removeComments

By default, all the comments in the source code are preserved in the output files. Let’s create a simple source file with a few comments.

// a.ts
/**
 * @desc Declare a variable `a`.
 */
var a = "A";

When we compile this program using tsc command, we get the a.js file that contains the following code.

// a.ts
/**
 * @desc Declare a variable `a`.
 */
var a = "A";

There isn’t much difference between the source code and the output. We still have all the original code comments preserved in the output.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "removeComments": true
    }
}

When we set the removeComments option to true as shown in the above tsconfig.json, these comments will be removed from the output.

var a = "A";

module

Let’s imagine if you are writing a TypeScript program that is going to run on Node.js. When you import a module in a TypeScript program, you use an import statement, but that’s not supported by Node.js (as of now). Node.js uses CommonJS module system which uses require() calls to import modules.

A compiled TypeScript program can run in an environment that implements its own module systems. For example, AMD (Asynchronous Module Definition) is quite popular in web applications to fetch JavaScript modules asynchronously when required. It uses define() call to import modules.

Since these environments can’t support ECMAScript import statements to load modules, the TypeScript compiler needs to convert import statements into require() or define() calls such that the compiled code can run in these environments.

The module compiler-option specifies the module system for which the TypeScript compiler needs to optimize the import statements. The supported values for this option are None, CommonJS, ES6 (_or _ES2015), UMD, AMD, System, ES2020, and ESNext.

// b.ts
export const b = "B";

// c.ts
import { b } from "./b";
console.log("b from b.ts is =>", b); // 'B'

In the above example, we have exported the constant b from b.ts and imported inside c.ts using import statement. If we want to run this program on Node, we want these import/export statements to get converted to require/exports statements. For that, we need to set module to CommonJS.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "module": "CommonJS"
    }
}

Using the above tsconfig.json file, when we compile our project, the compiled b.js and c.js output files look like below.

// b.js
exports.b = "B";

// c.js
var b_1 = require("./b");
console.log("b from b.ts is =>", b_1.b);

I have removed some unnecessary code from the above files but in a nutshell, the export const b statement got converted to exports.b and import './b' statement got converted to require('./b') call. Now we can run c.js program inside Node.js using node c.js command.

/projects/sample/dist/src
$ node c.js
b from b.ts is => B

The default value of module option depends on the target compiler-option value. When the target is ES3 or ES5, the default value for module is CommonJS. If the target value is ES6 or higher, default value ES6.

💡 For other module system examples, please visit this documentation.

outFile

The outFile compiler-option instructs the TypeScript compiler to combine all compiled .js files into one bundle and output a single .js file. The outFile option specifies the file path of this bundle file.

However, this option can not be used unless the module option is set to None, System or AMD. So you won’t be able to bundle CommonJS, ES6 or UMD modules. Perhaps, Rollup, Parcel, or Webpack can help you there.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "removeComments": true,
        "outFile": "dist/bundle.js",
        "module": "AMD"
    }
}

For the above tsconfig.json, the TypeScript compiler first converts all import statements into define() calls in the compiled JavaScript code since module is set to AMD and then bundles all the output code into bundle.js file at the location provided by the outFile compiler-option.

// dist/bundle.js
var a = "A";
define("src/b", ["require", "exports"], function (require, exports) {
  "use strict";
  exports.__esModule = true;
  exports.b = void 0;
  exports.b = "B";
});
define("src/c", ["require", "exports", "src/b"], function (
  require,
  exports,
  b_1,
) {
  "use strict";
  exports.__esModule = true;
  console.log("b from b.ts is =>", b_1.b);
});

You can implement a library such as RequireJS to use these AMD modules in browser environments. I would personally prefer the outFile option to bundle non-module code (_when module is _None) into a single file that I can run on Node or inside a browser.

💡 The reason CommonJS or ES6 modules can not be bundle has to do with the file import locations. If an import statement inside x.js (compiled file) looks like require('dir/y'), this means whenever this code runs, the y.js is imported from dir directory relative to x.js. When these files are bundled into a single file, it becomes unportable since x.js and y.js are now in the same directory (the same file actually). Bundlers like Webpack adds its own module loading code in the final bundle to make this work, however, TypeScript doesn’t.

Generating Souce-Maps

A source-map is a file that contains the mapping between the original source code and the generated output code. These files are extremely useful while debugging since it enables a browser to display original source code.

These files are automatically generated by a build tool such as the TypeScript compiler and they usually end with .map extension. A source-map file is a plain JSON file with the following schema.

// b.js.map
{
  "version": 3,
  "file": "b.js",
  "sourceRoot": "",
  "sources": ["./src/b.ts"],
  "names": [],
  "mappings": "CAAA,SAAAA,GAAA,GAAAA,..."
}

The file property indicates the output file name for which this source-map file was generated and sources property contains the source file paths from which the file file was generated. The mapping property contains the Base64 encoded values that hold the relationship between the source and output code.

💡 If you want to learn more about Source Maps, follow this article.

When a .map file is created for the mapping of a source file and the output .js file, a sourceMappingURL comment at added at the end of the output .js file as shown below. This comment is read by the browser while debugging and the b.js.map file is loaded from the network.

// b.js
var b = "B";
//# sourceMappingURL=b.js.map   ⫷

sourceMap

By default, when we run the tsc command, the TypeScript compiler doesn’t produce these source-map files along with the .js output files. We can enable this option by setting sourceMap compiler-option to true.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "sourceMap": true
    }
}

From the above tsconfig.json, the TypeScript compiler will produce source-map files for every input source file. These files will be placed adjacent to the compiled .js output files. These map files will have the same name as the output files with the .map extension. The output will look similar to below.

/projects/sample/
├── dist
|  ├── a.js
|  ├── a.js.map
|  └── src
|     ├── b.js
|     ├── b.js.map
|     ├── c.js
|     ├── c.js.map
|     └── lib
|        ├── d.js
|        ├── d.js.map
|        ├── e.js
|        └── e.js.map
├── a.ts
├── src/...
└── tsconfig.json

If we inspect b.js.map file, we would see the following output.

// b.js.map
{
  "version": 3,
  "file": "b.js",
  "sourceRoot": "",
  "sources": ["../../src/b.ts"],
  "names": [],
  "mappings": "AAAA,OAAO;AACP,IAAI,CAAC,GAAG,GAAG,CAAC"
}

The sources list contains ../../src/b.ts since it is the source file path for the b.js (_indicated by _file) relative to the b.js.map file (self).

mapRoot

In the previous example, the sourceMappingURL comment added to b.js output file provided the relative location of b.js.map file. When the debugger reads this comment, it fetches b.js.map file relative to the path of b.js.

//# sourceMappingURL=b.js.map

If your map files are located inside a separate directory or hosted on a completely different domain, then the sourceMappingURL must reflect the exact file path. For example, if the .map files are served at the debug.com/dist, then we need the following comment.

// b.js
var b = "B";
//# sourceMappingURL=http://debug.com/dist/src/b.js.map

The TypeScript compiler can automatically adjust this path based on the mapRoot property value. The mapRoot value points to the path from where all the .map files will be served.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "sourceMap": true,
        "mapRoot": "http://debug.com/dist/"
    }
}

For the above tsconfig.json, each source-map file path in the sourceMappingURL comment will be prepended with <http://debug.com/dist/>.

// b.js
var b = "B";
//# sourceMappingURL=http://debug.com/dist/src/b.js.map

The mapRoot option value is used as-is without any modification. However, you can use a relative path instead of a fully-qualified URL. In that case, the TypeScript compiler automatically adjusts the source-map file path relative to the compiled output file.

For example, if we use ./maps for the mapRoot value, then the sourceMappingURL comment for b.js would be as following. The ./maps path is relative to the tsconfig.json file, therefore, the path of the .map file in the comment was adjusted. What the TypeScript compiler assumes that the all .map files are located inside ./maps directory.

// b.js
var b = "B";
//# sourceMappingURL=../../maps/src/b.js.map

But you must remember that this option does not change where the .map files are generated. Same as previously, the .map file is generated where the compiled output file is located. The mapRoot merely instructs the debugger from where to fetch the source-map file. So you need to configure your server such that the debugger would be able to fetch these source-map files.

inlineSourceMap

The sourceMap option generates a separate .map file for a compiled .js output file. When you want to avoid generating a .map file, you can embed source-maps directly in the .js file itself.

{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "inlineSourceMap": true
    }
}

Instead of sourceMap, you can use inlineSourceMap compiler-option that embeds Base64 encoded source-map in the .js file.

// b.js
var b = "B";
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJ...ifQ==

Since non-inline source-maps are loaded while debugging, a server may reject a request that asks for .map files (perhaps dues to server configuration). In that case, inline source-maps can be helpful since source-maps are available in the .js file itself. However, they increase the size of .js file, so make a wise choice. In any case, inline source-maps should be avoided.

sourceRoot

The sourceRoot behaves similarly as the mapRoot compiler-option but instead, it adjusts the sourceRoot property value in the .map file. This value is used by the debugger to fetch the original sources from a particular location.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "sourceMap": true,
        "mapRoot": "http://debug.com/dist/",
        "sourceRoot": "http://source.com/"
    }
}

So for the above tsconfig.json, the output b.js.map file would look like below. As you can see, the sourceRoot value and the sources value is adjusted such that a debugger can fetch the original source code for b.js from http://source.com/src/b.ts.

// b.js.map
{
  "version": 3,
  "file": "b.js",
  "sourceRoot": "http://source.com/",
  "sources": ["src/b.ts"],
  "names": [],
  "mappings": "AAAA,OAAO;AACP,IAAI,CAAC,GAAG,GAAG,CAAC"
}

Similar to the mapRoot option, the TypeScript compiler will not change the location of source files. You need to configure your server such that the debugger would be able to fetch the original source files.

inlineSources

As we have seen, a source-map is a way to connect a compiled output code with its source code. This is immensely useful while debugging compiled code which sometimes can be unreadable.

The "sources" field in a source-map contains the list of source files that are fetched on-demand from the server while debugging. This is true for both inline and non-inline source-maps.

{
  "version": 3,
  "file": "x.js",
  "sourceRoot": "",
  "sources": ["./x.ts"],
  "names": [],
  "mappings": ""
}

If you want to avoid fetching these source files on demand, you can embed them inside a source-map itself. The inlineSources option, if set to true, inlines the sources inside the source maps.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "sourceMap": true,
        "inlineSources": true
    }
}

For the above tsconfig.json, the sourceMap property is set to true, which means the TypeScript compiler will generate .map file for each output .js file. The inlineSources is set to true so that we have inlined sources of these output .js files inside their corresponding .map files.

// b.js.map
{
  "version": 3,
  "file": "b.js",
  "sourceRoot": "",
  "sources": ["../../src/b.ts"],
  "names": [],
  "mappings": "AAAA,OAAO;AACP,IAAI,CAAC,GAAG,GAAG,CAAC",
  "sourcesContent": ["// b.js\nvar b = 'B';\n"]
}

As you can see from the above b.js.map file, the sourcesContent contains the corresponding source code of the source files listed inside sources.

// tsconfig.json
{
  "include": ["a.ts", "src/**/*"],
  "compilerOptions": {
    "outDir": "dist",
    "inlineSourceMap": true,
    "inlineSources": true
  }
}

Instead of sourceMap, if you have inlineSourceMap option enabled as shown in the above tsconfig.json file, then first the sources will be inlined inside a source-map and then these source-maps will be inlined inside .js files.

// b.js
var b = "B";
//# sourceMappingURL=data:application/json;base64,...fQ==

💡 If you are not convinced if the inlined source-map output is correct, then you can certainly decode Base64 encoded string (the part after base64, in the sourceMappingURL comment) using an online Base64 Decoder.

Type Declarations

A type declaration file contains the type information about an entity. For example, when you use Promise constructor in your code, your IDE displays information about the constructor signature (here Promise class is an entity). The declaration for the Promise API is provided from the TypeScript standard library. The TypeScript compiler also uses these declarations to raise exceptions when the Promise API is incorrectly implemented.

Similarly, when you define a variable without a type, the TypeScript compiler infers the type from the value being assigned to the variable.

// src/lib/d.ts
export const sayHello = function (name: string): string {
  return `Hello, ${name}.`;
};

In the above example, the inferred type of sayHello is function with (name: string) => string signature. You can also define a custom function type and provide an explicit type for the sayHello as shown below.

// src/lib/d.ts
type sayHelloFn = (name: string) => string;
export const sayHello: sayHelloFn = (name) => {
  return `Hello, ${name}.`;
};

In both cases, the TypeScript compiler has type information about sayHello so that it can check if this function is invoked with unsupported values or re-assigned with an unsupported function signature.

// tsconfig.json
{
  "include": ["a.ts", "src/**/*"],
  "compilerOptions": {
    "outDir": "dist"
  }
}

When you compile the d.ts program using the above tsconfig.json configuration, the output file d.js would be as following.

// dist/src/lib/d.js
exports.sayHello = function (name) {
  return "Hello, " + name + ".";
};

As you can see, we have lost all the type information in the compiled JavaScript. This normally wouldn’t be a problem if we intend to execute the program directly whether inside a browser or in Node.

However, when you are developing a plugin that your users are going to import in their TypeScript program, you normally publish an NPM package and have your users install it using npm install <pkg-name> command.

You can either ship the original TypeScript source code in the package or ship the compiled JavaScript code with the type declaration files. These files end with .d.ts extension and they are also called type definition files.

When your users import the sayHello function from the pkg-name package using import {sayHello} from '<pkg-name>' statement, the TypeScript compiler will only refer to these type declarations. Any wrong implementation of sayHello function will result in a compilation error.

💡 We have discussed the mechanism of how the TypeScript compiler imports NPM packages (node modules) in the Module System lesson.

declaration

By default, when you compile a TypeScript program, the TypeScript compiler does not generate type declaration files. You can enable the generation of these declaration files by setting declaration compiler-option to true.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "declaration": true
    }
}

When you run tsc command, the TypeScript compiler not only generates .js output files for a .ts source file but also generates .d.ts file alongside the .js file with the same name as the source file.

// src/lib/d.ts
type sayHelloFn = (name: string) => string;
export const sayHello: sayHelloFn = (name: string): string => {
  return `Hello, ${name}.`;
};

// dist/src/lib/d.js
exports.sayHello = function (name) {
  return "Hello, " + name + ".";
};

// dist/src/lib/d.d.ts
declare type sayHelloFn = (name: string) => string;
export declare const sayHello: sayHelloFn;
export {};

Using these declaration files, the TypeScript compiler knows about the API structure of your package even though your package contains compiled JavaScript code without any type information.

💡 We have discussed more about these declaration files and how we can import them in the Declaration Files lesson (coming soon).

declarationDir

When the declaration compiler-option is set to true, the TypeScript compiler puts the .d.ts declaration files where the compiled .js output file is generated.

/projects/sample/dist/
├── a.d.ts
├── a.js
└── src/
   ├── b.d.ts
   ├── b.js
   ├── c.d.ts
   ├── c.js
   └── lib/
      ├── d.d.ts
      ├── d.js
      ├── e.d.ts
      └── e.js

If you want to relocate all .d.ts files in a single directory, you can provide declarationDir option with the path to this directory.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "declaration": true,
        "declarationDir": "dist/types"
    }
}

With the above tsconfig.json configuration, all .d.ts files are stored inside dist/types directory (relative to tsconfig.json file) with the exact same directory structure of the sources.

/projects/sample/dist/types/
├── a.d.ts
└── src/
   ├── b.d.ts
   ├── c.d.ts
   └── lib/
      ├── d.d.ts
      └── e.d.ts

declarationMap

A declaration-map is like a source-map that holds the relationship between the generated declaration file and the actual source file from which the declaration file was generated.

// /projects/sample/types/src/lib/d.d.ts.map
{
  "version": 3,
  "file": "d.d.ts",
  "sourceRoot": "",
  "sources": ["../../../src/lib/d.ts"],
  "names": [],
  "mappings": "AAEA,aAAK,UAAU,GAAG,..."
}

💡 This declaration-map is used by the IDEs like VSCode to jump to the line in the source code (.ts file) when the user clicks a type in the declaration file.

Like a source-map of the compiled .js file, a declaration map ends with .map extension with the name same as the declaration file name. The TypeScript compiler puts the .d.ts.map file where its corresponding d.ts file is generated.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "declaration": true,
        "declarationDir": "types",
        "declarationMap": true
    }
}

So when you compile the project using the above tsconfig.json file, you will see declarations files (.d.ts) and declaration-map files (.d.ts.map) in the types directory like the following.

/projects/sample/dist/types/
├── a.d.ts
├── a.d.ts.map
└── src/
   ├── b.d.ts
   ├── b.d.ts.map
   ├── c.d.ts
   ├── c.d.ts.map
   └── lib/
      ├── d.d.ts
      ├── d.d.ts.map
      ├── e.d.ts
      └── e.d.ts.map

lib

TypeScript installation comes with build-in type declarations for JavaScript APIs such as Promise, Object.freeze, Array.prototype.map and much much more. TypeScript also provides type declarations for browser APIs such as DOM, fetch, Web Worker, setTimeout, console.log, etc.

These type declarations help us write valid JavaScript programs by providing hints and warnings. These type declaration files are generally be located inside lib directory of your standard TypeScript installation. You can also see these files from the official TypeScript repository.

When you compile a TypeScript program to JavaScript, it’s probably going to run inside a browser or on Node. The target compiler-option instructs the TypeScript compiler to check if the source code is compliant with the target.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "target": "ES5"
    }
}

For example, if the target is set to ES5 and our source-code contains the implementation of Promise JavaScript API which is not available in the ES5 version of the JavaScript, then the TypeScript compiler will throw an error.

$ tsc
Error TS2585: 'Promise' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the `lib` compiler-option to es2015 or later.
var p = new Promise( ( resolve, reject ) => {
            ~~~~~~~

As you can see from the above compilation output, the TypeScript compiler doesn’t allow the use of Promise when target is set to ES5 or below since Promise feature was introduced in ES2015. Hence you can set target to ES2015 or above and this program will compile just fine.

The above error suggests that we should change the lib compiler-option to ES2015 or later, but how does that work?

The target compiler-option controls many aspects of the TypeScript compiler, one of which is lib compiler-option. The lib value is a list of libraries to import from the standard library. These are basically the type declarations provided by the TypeScript.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "target": "ES5",
        "lib": [ "ES5", "ES2015.Promise", "DOM" ]
    }
}

In the above tsconfig.json file, we have mentioned that the JavaScript target for the compilation is ES5 but we want the TypeScript to allow declarations for Promise (but not all ES2015 features) and all DOM APIs (which represents all browser APIs and not just the Document Object Model).

The possible values for lib are as follows (could be more). The ES6 and ES2015 as well as ES7 and ES2016 represents the same library.

"ES5", "ES6", "ES2015", "ES7", "ES2016", "ES2017", "ES2018", "ESNext", "DOM", "DOM.Iterable", "WebWorker", "ScriptHost", "ES2015.Core", "ES2015.Collection", "ES2015.Generator", "ES2015.Iterable", "ES2015.Promise", "ES2015.Proxy", "ES2015.Reflect", "ES2015.Symbol", "ES2015.Symbol.WellKnown", "ES2016.Array.Include", "ES2017.object", "ES2017.Intl", "ES2017.SharedMemory", "ES2017.String", "ES2017.TypedArrays", "ES2018.Intl", "ES2018.Promise", "ES2018.RegExp", "ESNext.AsyncIterable", "ESNext.Array", "ESNext.Intl", "ESNext.Symbol"

In most of the cases, you should avoid a custom value for lib since the target option controls what libraries to import based on the features available in target JavaScript version. These default lib values corresponding to the target values are as follows.

For Target ES5    : "DOM", "ES5", "ScriptHost"
For Target ES2015 : "DOM", "ES6", "DOM.Iterable", "ScriptHost"

The reason you may still want to provide a custom lib value is when your runtime environment supports a JavaScript feature that is not supported in the target version. For example, if your runtime environment (such as the browser) supports Promise and Symbol through a polyfill then you may still want to compile a TypeScript program that uses promises and symbols.

You can either opt-in for a specific JavaScript feature such as Promise using "ES2015.Promise" library component or all the features of the ES2015 version as "ES2015" library. You can use this website to check available JavaScript features of a specific JavaScript version.

Since "DOM" library is always injected by default if no lib value is provided, you may want to disable that if your compiled JavaScript code is going to run in a non-browser environment such as Node. This will prevent any accidental use of DOM APIs such as window which doesn’t exist inside Node.

💡 As we have learned that the default value of lib is controlled by the target option, if you do not want TypeScript to include any libraries implicitly, set noLib compiler-option to true.

typeRoots

As we’ve learned that using the declaration compiler-option, you can generate type declaration files along with the compiled .js files.

When you publish your TypeScript program as an NPM module, you probably distribute a compiled JavaScript code such that a non-TypeScript program can import it or use it. You also provide type declarations with it so that if this module is imported inside a TypeScript program, your IDE and TypeScript compiler can enforce some rules based on these declarations.

💡 In the Module System lesson, we saw how the TypeScript compiler imports type declarations from an NPM module. Basically, the type declaration file in the package is indicated via types or typings property of the package.json.

You can either generate these type declaration files from the TypeScript source code using the declaration compiler-option or you can write them manually. The only thing that matters is that the TypeScript compiler should be able to import them.

Some of the popular NPM packages such as lodash or moment aren’t written in TypeScript, so generating type declarations from the source code goes out of the window. Also, these packages do not ship type declarations that are handwritten, so you really can’t expect type safety alone from these packages.

The initiative to provide type declaration for such packages have been taken by DefinitelyTyped community. You can install a package from @types NPM organization that contains the type declarations for a particular NPM package. These are package declarations (besides the standard library declarations provided by the TypeScript, specified using lib option).

💡 In the Declaration Files lesson (coming soon), we have discussed how we can write and these custom type declarations.

For example, npm install @types/lodash command will install @types/lodash package (inside node_modules/@types) that purely contains type declarations for the lodash package. This community maintains type declarations for thousands of packages (full list here).

By default, TypeScript imports type declarations of all the packages from the node_modules/@types directory even though the package is not imported in our source code. This is useful for providing auto-import IntelliSense.

According to the Node module resolution strategy (discussed here), a package is searched inside every node_modules directory by traversing the directory up until the final directory is reached. Hence TypeScript will try to import type declarations from all these node_module directories.

A type-root is a directory where package declarations can be found. The typeRoots compiler-option specifies a list of directory locations where these package declaration can be found. The default value of this option is node_modules/@types (for all node_module directories).

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "typeRoots": [ "./my-types", "./vendor" ]
    }
}

Using the above tsconfig.json configuration, TypeScript will only import declarations from the packages listed inside my-types and vendor directories relative to the tsconfig.json file and not from the node_modules directories.

types

The types compiler-option specifies only those packages declarations that should be imported from the type-roots.

// tsconfig.json
{
  "include": ["a.ts", "src/**/*"],
  "compilerOptions": {
    "outDir": "dist",
    "types": ["node", "moment"]
  }
}

Using the above tsconfig.json configuration, TypeScript will only auto-import declarations from the <type-root>/node and the <type-root>/moment packages. This will prevent IDEs to display auto-import hints for packages other than node or moment.

However, this does not prevent type declaration import of a package when the package is imported manually. For example, when you use import lodash using import 'lodash' statement, TypeScript will fetch type declarations from the <type-root>/lodash.

JavaScript Compilation

Not only .ts and .d.ts files, but you can also include .js file in the compilation process. By default, this feature is disabled so that you don’t accidentally include .js files but you can enable it via allowJS compiler-option.

Normally you want to compile TypeScript (.ts files) to JavaScript (.js files). You include them in the compilation process using files or include root-level option. You can also import these files using import statements that automatically include them for the compilation.

/projects/sample/
├── a.ts
├── p.js   ⫷
├── src/
|  ├── b.ts
|  ├── c.ts
|  ├── q.js   ⫷
|  └── lib/
|     ├── d.ts
|     └── e.ts
└── tsconfig.json

But sometimes you have a third party JavaScript program that you want to import in a TypeScript program using an import statement. However, the TypeScript compiler only searches for src/q.ts and src/q.d.ts when your import statement is import 'src/q'.

// a.ts
import { q } from "./src/q";
console.log(q);

So if you want to import src/q.js, the TypeScript compiler won’t allow this when strict compiler-option is set to true. If we compile the above program, you are going to get the following error.

$ tsc
Error TS7016: Could not find a declaration file for module './src/q'. '/programs/sample/src/q.js' implicitly has an 'any' type.

We are going to talk about strict compiler-option in a bit, but this error tells that the TypeScript compiler couldn’t find ./src/q.ts or ./src/q.d.ts file, hence it could not obtain the declarations for the exports. So imports for this module received any type by default (q export value in this example) which is not allowed when strict is true.

Similarly, though you may have included .js files using files or include root-level option in the tsconfig.json, the TypeScript compiler will simply ignore them during the compilation.

// tsconfig.json
{
  "include": ["a.ts", "p.js", "src/**/*.ts", "src/**/*.js"],
  "compilerOptions": {
    "outDir": "dist",
    "strict": true
  }
}

If you remove the import statement from a.ts (to avoid previous compilation error) and compile the project using the abovetsconfig.json configuration, you will see the below files in the dist directory.

/projects/sample/dist/
├── a.js
└── src/
   ├── b.js
   ├── c.js
   └── lib/
      ├── d.js
      └── e.js

As you can see, the TypeScript compiler simply dropped the p.js and src/q.js from the compilation though the tsconfig.json included them. Therefore dist directory doesn’t contain p.js and src/q.js.

allowJs

The allowJs compiler-option tells the TypeScript compiler to include .js files in the compilation process as well. This applies to both files and include options as well as import statements.

// tsconfig.json
{
    "include": [
        "a.ts",
        "p.js",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "strict": true,
        "allowJs": true
    }
}

From the above tsconfig.json, since the p.js file is included in the compilation, it will be considered in the compilation and TypeScript will output that file in the final build.

/projects/sample/dist/
├── a.js
├── p.js
└── src/
   ├── b.js
   ├── c.js
   └── lib/
      ├── d.js
      └── e.js

Similarly, since allowJs option is set to true, the TypeScript compiler will look for a .js file when an import statement is encountered.

// a.ts
import { q } from "./src/q";
console.log(q);

// src/q.js
export const q = parseInt(1.5);

With the same tsconfig.json, you will get the following build files.

/projects/sample/dist/
├── a.js
├── p.js
└── src
   ├── b.js
   ├── c.js
   ├── q.js
   └── lib
      ├── d.js
      └── e.js

Due to the import statement, q.js also became part of the compilation.

Unlike a TypeScript program, a JavaScript program doesn’t contain type annotation. Therefore TypeScript infers types by statically analyzing the program. Whenever TypeScript can not infer types, any type is used.

// a.ts
import { getInteger } from "./src/q";
getInteger(true).toUpperCase();

// src/q.js
export function getInteger(value) {
  return parseInt(value);
}

In the above example, q.js exports the function getInteger which takes a value as an argument and returns a number value returned by the parseInt function. So TypeScript can infer the return type of getInteger but not the value argument. Therefore, getInteger gets the following types.

function getInteger(value: any): number;

When we compile the project, we are going to get the following error.

$ tsc
a.ts:3:20 - error TS2339: Property 'toUpperCase' does not exist on type 'number'.
getInteger( true ).toUpperCase();
                   ~~~~~~~~~~~

This compilation error occurred since we are trying to call toUpperCase method on a value of type number. The true argument value to getInteger is not valid as well but since its type was inferred as any, TypeScript allows it.

The allowJs option is normally used when you want to migrate your JavaScript project to TypeScript. Since JavaScript is a subset of TypeScript, any valid JavaScript program is a TypeScript program. Once the initial project setup is complete, you can start migrating each .js file into a .ts file without having to wait for a complete project revamp.

checkJs

Though allowJs , we can allow JavaScript files to be a part of the compilation, but they are not type-checked during the compilation. In the earilier example, q.js exported q which is a return value of parseInt(1.5).

However, parseInt JavaScript function takes an argument of type string which would have caught by the TypeScript compiler had it been a TypeScript program. This proves that the TypeScript compiler doesn’t type-check JavaScript programs by default.

To allow the TypeScript compiler to evaluate JavaScript programs as well for any type issues, we set checkJs to true along with allowJs.

// tsconfig.json
{
    "include": [
        "a.ts",
        "p.js",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "strict": true,
        "allowJs": true,
        "checkJs": true
    }
}

So for the above tsconfig.json file, the TypeScript will check any type related issues in p.js as well as src/q.js since it is imported inside a.ts.

$ tsc
src/q.js:1:28 - error TS2345: Argument of type '1.5' is not assignable to parameter of type 'string'.
export const q = parseInt( 1.5 );
                           ~~~

Downleveling

TypeScript through its official documentation clarifies that its design goal is to provide a cross-platform development tool to statically analyze JavaScript code and not as a means to generate JavaScript that runs consistently well across all platforms.

Though TypeScript is a transpiler, it doesn’t guarantee runtime performance and consistency which is one of the design goals of Babel. That means when you write a TypeScript program, you better know if that program is going to run well across multiple platforms such as IE, Chrome, Safari, etc.

Downlevel is a process of transforming a feature implementation of a programming language (that might not be available on all the platforms) into something that works well across platforms. It is also known as down compiling. For example, ES6 Arrow Functions can be transformed into normal function expressions that have supports across all browsers.

TypeScript downlevels some of the features based on the target compiler-option value. However, TypeScript doesn’t add polyfills on its own. For example, you might implement Promise in your program but TypeScript won’t add a polyfill for it even if your target is set to ES5 or below.

target

The target compiler-option specifies the target JavaScript version for the compilation. The default value of target option is ES3 which means your code will be analyzed as per the specifications of ES3.

TypeScript automatically optimizes the value for the lib option based on the current target value. We learned about this process and how to override this behavior in the lib compiler-option section.

Apart from controlling the libraries according to the value of target option, TypeScript also downlevels new JavaScript features in the flavor of the target JavaScript version. Here are the possible values for the target.

'ES3' (default), 'ES5', 'ES6/ES2015', 'ES7/ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ESNext'

Let’s take a look at some basic programs and compare the compiled JavaScript code for multiple targets.

// a.ts (source)
const sayHello = (name: string): string => {
  return `Hello, ${name}.`;
};

// a.js (target: "ES6")
const sayHello = (name) => {
  return `Hello, ${name}.`;
};

// a.js (target: "ES5")
var sayHello = function (name) {
  return "Hello, " + name + ".";
};

In the above example, a.ts uses ES6 features like const, Arrow function and Template Strings. For the ES5 target, the TypeScript compiler downlevels these features to using best suitable features available in ES5.

However, for the ES6 target, it doesn’t have to do anything since all the features implemented in the source program are available in the target JavaScript version. So the original program is left intact in the output.

// a.ts (source)
const promise =
  new Promise() <
  symbol >
  ((resolve) => {
    resolve(Symbol("SAMPLE"));
  });

// a.js (target: "ES6")
const promise = new Promise((resolve) => {
  resolve(Symbol("SAMPLE"));
});

// a.js (target: "ES5")
var promise = new Promise(function (resolve) {
  resolve(Symbol("SAMPLE"));
});

Things look different for the above example. Now our source program a.ts uses Promise and Symbol which are available in ES6+ but not in ES5. These can be downleveled but TypeScript won’t do that. Since these features are fully polyfillable, TypeScript leaves that to us.

So in the end, TypeScript compiler only transforms const and Arrow function while leaving Promise and Symbol intact. Therefore it becomes our responsibility to add polyfills for them at the runtime.

TypeScript can choose a suitable approach to down compile something based on the target value. For example, async/await syntax for the ES6/ES7 target is down compiled using Promise and Generator. Since Promise and Generator is available in these versions, we don’t have to do anything.

However, when the target is set to ES3/ES5, it uses Promise and Symbol.iterator. This means runtime needs to have polyfills for the Promise and Symbol in order to run this program. If Symbol polyfill is not present at the runtime, then a state machine emulator logic is used for the iteration. In both cases, Promise much be available at the runtime.

// a.ts (source)
var fetchData = async () => {
  const response = await fetch("http://example.com");
  console.log(response);
};

So for the above source program, the TypeScript compiler generates the following JavaScript code.

downlevelIteration

We have been using the traditional for loop for a long time to iterate over a finite range. The for/in loop makes iteration possible for objects. However, ES6 gave us new means to perform iterations. These are for/of loop, spread operator, and Symbol.[iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator).

Let’s write a program that uses for/of loop and spread operators and see how its compiled JavaScript code looks like for ES6 and ES5 target .

// a.ts (source)
for (let num of [1, 2, 3]) {
  console.log(num);
}

// a.js (target: "ES6")
for (let num of [1, 2, 3]) {
  console.log(num);
}

// a.js (target: "ES5")
for (var _i = 0, _a = [1, 2, 3]; _i < _a.length; _i++) {
  var num = _a[_i];
  console.log(num);
}

Normally, the TypeScript compiler downlevels the for/of loop using traditional for loop. This seems perfectly valid but it is not 100% compliant with the spec of for/of loop. You need to read this article to understand what the issue is but, in a nutshell, the traditional for loop doesn’t play well with some Unicode characters.

The downlevelIteration compiler-option when is set to true tells the TypeScript compiler to downlevel for/of iterations in the source program using Symbol.iterator. So at the runtime, if the Symbol.iterator is present, the compiled program is going to use it to emulate the correct for/of behavior, else it will fallback to traditional for loop.

So for the above a.ts source program and tsconfig.json configuration, the TypeScript compiler produces the following JavaScript code.

This is the same is the story with the spread operator.

// a.ts (source)
var odd = [ 1, 3, 5 ];
var even = [ 0, 2, 4, ...odd ];

// a.js (target: "ES6")
var odd = [ 1, 3, 5 ];
var even = [ 0, 2, 4, ...odd ];

// a.js (target: "ES5")
var __spreadArrays = (this && this.__spreadArrays) || function () {
    ...
};
var odd = [1, 3, 5];
var even = __spreadArrays([0, 2, 4], odd);

Normally, the TypeScript compiler downlevels spread syntax into a __spreadArrays function call. This function implements logic to spread an array using for loop. The TypeScript compiler may use Array.concat prototype function call when best suitable. But this transpilation isn’t 100% compliant with the spread syntax.

When The downlevelIteration compiler-option when is set to true, the TypeScript compiler uses Symbol.iterator to best mimic the spread operator.

importHelpers

As we have seen so far, downleveled code is not pretty. TypeScript compiler usually injects helper function such as __spread and __read we saw in the previous example to mimic spread syntax or __generator and __awaiter we saw in the async/await example.

TypeScript does this for every single source file. Therefore every compiled .js file will contain these helper functions unless a bundle is being generated (using outFile compiler-option).

To avoid this code duplication, TypeScript provides tslib NPM package that contains all these helper functions. The compiled JavaScript code can import these helper functions from the tslib package.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "target": "ES5",
        "module": "CommonJS",
        "downlevelIteration": true,
        "importHelpers": true
    }
}

By setting importHelpers compiler-option to true, the TypeScript compiler does not emit helper functions, instead, it adds import statement for the tslib package based on what module system you have chosen. So when module is set to CommonJS, you will see the following output code.

// a.ts (source)
var odd = [1, 3, 5];
export var even = [0, 2, 4, ...odd];

// a.js (target: "ES6")
var odd = [1, 3, 5];
exports.even = [0, 2, 4, ...odd];

// a.js (target: "ES5")
var tslib = require("tslib");
var odd = [1, 3, 5];
exports.even = tslib.__spread([0, 2, 4], odd);

As you can see from the above results, the TypeScript compiler only injected tslib package import statement for the ES5 target since ES6 supports spread syntax natively. So in the case of ES5 target, you need to make sure the runtime has access to the tslib package (means you have installed it).

TypeScript will only import helper function from tslib package in the final compiled code as long as importHelpers compiler-option is set to true and a source-file is a module. For this reason, we have added export var even statement to convert a.ts from a global script to a module.

💡 If you want to know more about the difference between a script file and a module, follow the Type System lesson.

noEmitHelpers

the importHelpers compiler-option prevents the injection of helper functions in the compiled JavaScript code. Instead, the compiler adds import/require statement of tslib package which contains the actual implementation of these helper functions.

There are a few drawbacks of using importHelpers option. First of all, you need to implement a module system so that tslib package is available at the runtime and second, the TypeScript compiler only does this for the modules and not for the global scripts.

The noEmitHelpers also prevents the injection of helper functions but it doesn’t add the tslib module import statements. It leaves the implementation of these helper functions to us. Let’s see an example.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "target": "ES5",
        "downlevelIteration": true,
        "noEmitHelpers": true
    }
}
// a.ts (source)
var odd = [1, 3, 5];
var even = [0, 2, 4, ...odd];

// dist/a.js
var odd = [1, 3, 5];
var even = __spread([0, 2, 4], odd);

In the above example, since the target is set to ES5 which doesn’t support the spread operator and downLevelIteration is set to true, the TypeScript compiler uses __spread helper function to emulate spread syntax.

However, it did not inject an implementation of these helper function in the compiled JavaScript code since noEmitHelpers is set to true. Also, it didn’t inject tslib module import statement since importHelpers option value is false by default.

💡 If you notice, the a.ts file is a global script since it doesn’t have neither an import nor an export statement. This clarifies that when noEmitHelpers is true, the TypeScript compiler uses helpers for global scripts as well.

Now the implementation of __spread helper function is up to us. We can write our own __spread function and make it available globally (for example, window.__spread).

Import Settings

These compiler options control how modules and input files are looked up by the TypeScript compiler.

moduleResolution (deprecated)

The moduleResolution option controls how an a module is resolved when the TypeScript compiler encounters import statement. You could be familiar with Node’s module resolution strategy where import 'lodash' statement looks for the node_modules directory and searches for the lodash package inside it.

// tsconfig.json
{
    "include": [
        "src/**/*"
    ],
    "compilerOptions": {
        "moduleResolution": "Node"
    }
}

Earlier versions of the TypeScript supported Classic strategy but it has been deprecated. Now, we only use Node which is the default strategy used by TypeScript if you don’t specify this option in the tsconfig.json file. With the Node strategy, the TypeScript compiler looks for the modules in your system exactly how Node.js would look.

💡 We have discussed about these strategies in the Module System lesson.

resolveJsonModule

As per the ECMAScript specifications, you can only import .js files (JavaScript) using import syntax. Node.js allows imports of the .json files as well using require() statement such as var obj = require('./a.json').

// a.ts
import { name } from "./a.json";
console.log(name);

However, if you import a .json file using import statement in a TypeScript program as shown in the example above, the TypeScript compiler throws an error indicating that it can not import .json files.

$ tsc
Error TS2732: Cannot find module './a.json'. Consider using '--resolveJsonModule' to import module with '.json' extension
import { name } from './a.json';
                      ~~~~~~~~~~

To enable imports of JSON modules, we need to set resolveJsonModule compiler-option to true. This option instructs the TypeScript compiler to allow .json files in the import statements.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "module": "CommonJS",
        "resolveJsonModule": true
    }
}

With the above tsconfig.json file, the TypeScript compiler not only compiles the a.ts to a.js with the following code but also moves imported .json file to the dist directory.

// dist/a.js
var a_json = require("./a.json");
console.log(a_json.name);

esModuleInterop

ECMAScript module system and CommonJS module system (employed by Node.js) isn’t fully compatible with each other. According to ECMAScript specifications, default import in a.ts is only allowed when a default export is specified by the module b.ts.

// a.ts (TS)
import hello from "./src/b";
console.log(hello); // HELLO WORLD

// src/b.ts (TS)
export default "HELLO WORLD";

Similarly, this exact example would look like this using CommonJS modules.

// p.js (JS)
const hello = require("./src/q");
console.log(hello); // HELLO WORLD

// src/q.js (JS)
module.exports = "HELLO WORLD";

The module.exports is equivalent to exports default of ECMAScript. When you set allowJs to true, you can import a JavaScript module inside a TypeScript program. TypeScript tries its best to interpolates between CommonJS and ECMAScript module syntax to make this happen.

// a.ts (TS)
import hello from "./src/q";
console.log(hello);

// src/q.js (JS)
module.exports = "HELLO WORLD";

But when you compile this program, you are likely to get the following compilation error.

a.ts:1:8 - error TS1259: Module '"/projects/sample/src/q"' can only be default-imported using the 'esModuleInterop' flag
import hello from './src/q';
       ~~~~~

The TypeScript compiler warns when we try to import the default export value from a module that doesn’t explicitly export the default value as per ECMAScript specifications.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "module": "CommonJS",
        "allowJs": true,
        "esModuleInterop": true
    }
}

By setting esModuleInterop compiler-option to true we are telling the TypeScript compiler to increase interoperability between these two module systems. When this option is true, TypeScript emits a helper function that extracts the default value from a CommonJS or JSON module.

// dist/a.js
var __importDefault = function (mod) {
  return mod && mod.__esModule ? mod : { default: mod };
};
exports.__esModule = true;
var q_1 = __importDefault(require("./src/q"));
console.log(q_1["default"]);

// dist/src/q.js
module.exports = "HELLO WORLD";

Here, the __importDefault is a helper function that extracts the default value from a JavaScript module. The TypeScript compiler automatically adds __esModule export in the compiled JavaScript if it is a CommonJS module that was compiled from an ECMAScript module. This is used by the __importDefault helper function to decide how to import the default value.

💡 Apart from __importDefault helper function, TypeScript might inject other helper functions such as __importStar to support import * as syntax. You can use importHelpers option to import these helpers from tslib package.

allowSyntheticDefaultImports

By default, the TypeScript compiler throws an error when you try to import the default value from a module that doesn’t explicitly have a default export as seen in the previous example. The allowSyntheticDefaultImports compiler-option, when set to true, suppresses this error.

When esModuleInterop is true, it also implicitly sets the value of allowSyntheticDefaultImports to true, that’s why we don’t receive any errors. The esModuleInterops’s main job is to emit helper functions to allow default imports at the runtime. However, the allowSyntheticDefaultImports doesn’t change emitted JavaScript code, it only allows default imports from a non-ECMAScript module.

baseUrl

// a.ts
import { b } from "./src/b";
console.log(b);

// src/b.ts
export const b = "B";

When you specify a relative module import such as the above, you specify a relative path of the module relative to the file that imports it. Therefore, ./src/b path represents the file b.ts (or b.d.ts) situated in the src directory (which is located in the directory of the current module).

// a.ts
import { b } from "src/b";

If we remove the ./ path prefix from the module path (as shown above), the TypeScript compiler is going to think that we are trying to import an NPM module so it will look for src/b module inside node_module directories.

If this module is not present in the node_modules directory, the TypeScript compiler will throw an error since it can’t find the module.

a.ts:1:19 - error TS2307: Cannot find module 'src/b' or its corresponding type declarations.
import { b } from 'src/b';
                  ~~~~~~~

The baseUrl option tells the TypeScript compiler a directory where the non-relative module imports without leading ./ or ../ should be looked at first.

// tsconfig.json
{
  "include": ["a.ts", "src/**/*.ts"],
  "compilerOptions": {
    "outDir": "dist",
    "baseUrl": "."
  }
}

Since we have specified the baseUrl compiler-option with . (or *./*) value, the TypeScript compiler will first look for src/b module inside the directory where tsconfig.json file (that’s .) is located and if it couldn’t find it, it will move to the node_modules directory for the module search.

Since we have the b.ts file in the src directory which is located in the directory pointed by the baseUrl, the TypeScript compiler will import it whenever it encounters src/b from anywhere in the project.

This option is very useful if you want to avoid relative paths in your project which could a nightmare while restructuring the project. For example, import '../../../../x' is just a horrible way to import a module. Instead, you can write import 'modules/x' and point baseUrl to a path where modules directory is located.

The TypeScript compiler doesn’t modify the import paths in the compilation as you can see from the below JavaScript code. TypeScript assumes that your runtime has the ability to resolve modules, hence the ask for baseUrl.

/dist/a.js
var b_1 = require("src/b");
console.log(b_1.b);

This could be a problem since Node.js will try to resolve the module src/b from the node_modules directory but its actual location is in the project directory itself. Let’s run the above program using node.

$ node dist/a.js
Error: Cannot find module 'src/b'.

As you can see, Node could not find src/b module in node_modules directory. Despite the fact that ./src/b.js file exists relative to a.js, for it, 'src/b' path means an NPM package inside node_modules directory.

What we need to do is to tell Node that there is another directory besides node_modules from where the modules should be loaded. We provide such directories using NODE_PATH environment variable.

The NODE_PATH is an environment variable just like system PATH through you can concatenate multiple directories using : (colon). However, NODE_PATH is used by Node to locate modules on the system if it can’t find them inside node_modules directories.

$ NODE_PATH="./dist" node dist/a.js
B

In the above command, we have specified the dist directory from where the modules should be resolved if Node can’t find them inside node_modules directory. This program works since src/b module (resolved to src/b.js) exists inside dist directory.

paths

The paths compiler-option is used in the conjugation with baseUrl option to re-map modules. For example, if we want to import 'box' to resolve to src/b module during the development, then we need to remap box to src/b in the paths option of your tsconfig.json.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "baseUrl": ".",
        "paths": {
            "box": [ "src/b" ]
        }
    }
}

The paths field is an object whose each key is the module name used in the import statements and the value is a list of module paths relative to the baseUrl that should be used for the module resolution. So form the above configuration, box module is resolved to src/b.

// a.ts
import { b } from "box";
console.log(b);

// dist/a.js
var box_1 = require("box");
console.log(box_1.b);

This creates a similar problem we just solved for the baseUrl option. The TypeScript compiler doesn’t change the module’s import path in the output when the paths options are provided. As you can see box module path remained intact in the output.

$ node dist/a.js
Error: Cannot find module 'box'

When we run a.js using node command, it’s gonna give module not found error since box module doesn’t exist in node_modules. We can’t fix this at the runtime merely by providing NODE_PATH environment variable since a module with the name box doesn’t exist anywhere.

This can be solved by aliasing the box module with the dist/src/b module at the runtime. There are tons of tools available to do this. The popular one seems to be module-alias package.

// package.json
{
  "name": "sample",
  "_moduleAliases": {
    "box": "dist/src/b"
  },
  "devDependencies": {
    "module-alias": "^2.2.2"
  }
}

You should provide the aliases in the package.json as shown above and import the module-alias/register.js script to register the aliases. My way of registering the aliases is by requiring this script in the command itself.

$ node -r module-alias/register dist/a.js
B

However, though this works in this case, it’s a manual effort of specifying and maintaining aliases in the package.json as well as tsconfig.json which aren’t guaranteed to work interchangeably at the runtime.

We have the tsconfig-paths package just to handle module aliases provided by the paths compiler-option. This package also provided register script which picks the aliases directly from the tsconfig.json.

$ node -r tsconfig-paths/register dist/a.js
Error: Cannot find module 'box'

Oops, we got the same error. What is the issue now? The problem is tsconfig-paths does not work well with outDir option for the node command. You can track this bug here. The fix is to change the baseUrl option.

// tsconfig.node.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "baseUrl": "dist"
  }
}

We have created a new tsconfig.node.json configuration file that extends earlier tsconfig.json file and only overrides baseUrl value. Using TS_NODE_PROJECT environment variable, we can specify the path of tsconfig file tsconfig-paths package should consider.

$ TS_NODE_PROJECT="./tsconfig.node.json" node -r tsconfig-paths/register dist/a.js
B

We can also specify wildcard aliases using paths option.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "baseUrl": ".",
        "paths": {
            "modules/*": [ "src/*" ]
        }
    }
}

For the above tsconfig.json, a module path such as modules/x will be resolved to src/x where x is the name of the module (or a file).

// a.ts
import { b } from "modules/b";
console.log(b);

So for the above program, modules/b will be resolved to src/b. When you compile this program, the TypeScript compiler doesn’t change the import path as usual. But this scenario is also handled by tsconfig-paths.

$ TS_NODE_PROJECT="./tsconfig.node.json" node -r tsconfig-paths/register dist/a.js
B

rootDirs

The rootDirs compiler-option behaves somewhat like the baseUrl option. The baseUrl option specifies a path relative to which non-relative module paths (that do not being with . or ..) should be resolved in case module is not located inside node_modules.

The rootDirs option specifies a list of directories that should be used for resolution when relative modules paths that begin with . or .. are used. The way it works is a little different. All the directories specified by the rootDirs option form a virtual directory.

// a.ts
import { b } from "./b";
console.log(b);

The above program is invalid since b.ts file is located inside ./src, hence TypeScript wouldn’t be able to locate ./b module. However, if we instruct the TypeScript compiler if . and ./src is the same directory path, then it will work since ./src/b.ts and ./b.ts would be the same thing.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "rootDirs": [
            ".",
            "./src"
        ]
    }
}

In the above tsconfig.json configuration, the rootDirs specifies . and ./src (relative to tsconfig.json file). Therefor . and ./src will form a single virtual directory and TypeScript will compile the program just fine.

// dist/a.js
var b_1 = require("./b");
console.log(b_1.b);

// dist/src/b.js
exports.b = "B";

Even for the rootDirs option, the TypeScript compiler does not change the module path in the compiled code or change the location of the module files in the output files. That means it is our responsibility to add a workaround for this so that this program works at the runtime.

Strict Options

In the strict mode, the TypeScript compiler enforces some additional rules during compilation. These options enable these rules.

strict

The strict compiler-option, when set to true, enables all the following rules. I would personally recommend setting this option to true and let the TypeScript enforce all the rules. If you need to disable a particular rule, you can set it to false.

alwaysStrict

ECMAScript 5 introduced Strict Mode which is a way to avoid spooky behavior of JavaScript. For example, if a variable is not declared in a scope and if we try to assign a value, JavaScript creates a global variable by default.

console.log("BEFORE: window.x =>", window.x);
(function () {
  x = 100;
})();
console.log("AFTER: window.x =>", window.x);

// > BEFORE: window.x => undefined
// > AFTER: window.x => 100

From ES5 onwards, JavaScript engines honor "use strict"; annotation which enables the strict mode for JavaScript. In strict mode, a variable won’t be created implicitly as shown in the following example.

"use strict";
console.log( 'BEFORE: window.x =>', window.x );
(function() {
    x = 100;
})();
console.log( 'AFTER: window.x =>', window.x );

// > Uncaught ReferenceError: x is not defined

Apart from this simple use case, strict mode disables many JavaScript operations that would have failed silently without any errors. You can read more about the strict mode on this MDN documentation.

In the earlier examples, we saw that the TypeScript compiler doesn’t insert "use strict"; annotation in the compiled JavaScript file. However, you can enable this by setting alwaysStrict compiler-option to true.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*"
    ],
    "compilerOptions": {
        "alwaysStrict": true
    }
}

Using the above tsconfig.json, when we run tsc command, the a.ts file is compiled into a.js with the following code.

"use strict";
// a.ts
/**
 * @desc Declare a variable `a`.
 */
var a = "A";

noImplicitAny

TypeScript by default uses any type for the values if these values haven’t been annotated with a type. This can cause some problems.

// a.ts
var s;
console.log(s.toUpperCase());

In the above example, we have defined the variable s but didn’t provide a type for it. Therefore, the TypeScript compiler will consider s of type any. Since any represents all possible values, s.toUpperCase() is valid but at runtime, this will fail since s is undefined and not a string.

To prevent TypeScript annotating values with any type if they lack an explicit type, we should set noImplicitAny compiler-option to true (if strict option is not set to true already).

// tsconfig.json
{
  "include": ["a.ts", "src/**/*.ts"],
  "compilerOptions": {
    "outDir": "dist",
    "noImplicitAny": true
  }
}

Using the above tsconfig.json file, if we compile the project, the TypeScript compiler now throws an error.

$ tsc
a.ts:2:14 - error TS2532: Object is possibly 'undefined'. console.log( s.toUpperCase() );
             ~

★ Others

TypeScript also provides other strict options such as noImplicitThis to prevent using this if it resolves to any type as well as the strictNullChecks option which is used to prevent working on possibly a null or undefined value. You should learn more about these options from this documentation.

Linter Options

These options let you enforce some rules that have more to do with the developer experience and code style.

noUnusedParameters

The noUnusedParamaters compiler-option instructs the TypeScript compiler to throw an error if it finds a function parameter unused in the function body that would have otherwise silently ignored by the TypeScript compiler.

// a.ts
function sayHello(name: string): string {
  return "Hello World";
}
// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "noUnusedParameters": true
    }
}

In the above program, the name parameter is not used in the function implementation. Since noUnusedParameters option is set to true, the TypeScript compiler won’t compile the program unless the error is resolved.

$ tsc
a.ts:1:20 - error TS6133: 'name' is declared but its value is never read.
function sayHello( name: string ) {
                   ~~~~

noUnusedLocals

Similar to noUnusedParameters, the noUnusedLocal option, if set to true, disallows leaving behind unused local variables that would have otherwise silently ignored by the TypeScript compiler.

// a.ts
function sayHello(name: string): string {
  const _name = name.toUpperCase();
  return "Hello World";
}
// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "noUnusedLocals": true
    }
}

In the above program, the _name local variable is not used in the function implementation. Since noUnusedLocals option is set to true, the TypeScript compiler won’t compile the program unless the error is resolved.

$ tsc
a.ts:2:11 - error TS6133: '_name' is declared but its value is never read.
const _name = name.toUpperCase();
      ~~~~~

noImplicitReturns

The noImplicitReturns checks if a function returns a value in all possible scenarios. TypeScript compiler evaluates the return type of a function by statically analyzing the control-flow tree. This is called control-flow based type analysis which is occurring statically (without executing the code).

If any node of this control-flow tree exits the function without returning a value, the TypeScript compiler will throw a compilation error if the noImplicitReturns option is set to true which would have otherwise silently ignored by the TypeScript compiler.

// a.ts
function sayHello(name: string): string {
  if (name !== "none") {
    return `Hello, ${name}.`;
  }
}
// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "noImplicitReturns": true
    }
}

In the above example, the sayHello function only returns a value if name argument is not equal to 'none'. Otherwise, it would be undefined. Since we have set noImplicitReturns option to true, we will get the following error.

$ tsc
a.ts:1:36 - error TS7030: Not all code paths return a value.
function sayHello( name: string ): string {
                                   ~~~~~~

noFallthroughCasesInSwitch

The noFallthroughCasesInSwitch enforces a case block in the switch statement to either have break or return statement. This is a way to prevent accidental execution of other case blocks.

// a.ts
function sayHello(name: string) {
  switch (name) {
    case "Ross": {
      console.log("Hello Doctor!");
    }
    case "Monica": {
      console.log("Hello Chef!");
    }
  }
}
// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "noFallthroughCasesInSwitch": true
    }
}

In the above example, the first case block doesn’t have a break or return statement which means when it executes, the case below it will also get executed. However, since noFallthroughCasesInSwitch is set to true, the TypeScript compiler will throw this error instead.

$ tsc
a.ts:3:9 - error TS7029: Fallthrough case in switch.
case 'Ross': {
  ~~~~~~~~~~~~

Tooling Options

These options generally used by external tools to provide a layer of abstraction over the TypeScript compiler or to create a side-effect.

extends

This is a root-level option that is used to inherit configuration from other tsconfing file. The extends option specifies a path of another configuration file. All the objects are merged deeply while arrays are overridden.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "baseUrl": ".",
        "paths": {
            "modules/*": [ "src/*", "src/lib/*" ]
        }
    }
}

// tsconfig.prod.json
{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "outDir": "dist_prod",
        "paths": {
            "modules/*": [ "node_modules/*" ]
        }
    }
}

In the above example, tsconfig.prod.json inherits tsconfig.json file from the same directory. When you compile the TypeScript project using the tsconfig.prod.json file, the TypeScript compiler is going to receive the following configuration settings.

$ tsc --showConfig --project tsconfig.prod.json
{
    "compilerOptions": {
        "outDir": "./dist_prod",
        "baseUrl": "./",
        "paths": {
            "modules/*": [
                "node_modules/*"
            ]
        }
    },
    "files": [
        "./a.ts",
        "./src/b.ts",
        "./src/c.ts",
        "./src/lib/d.ts",
        "./src/lib/e.ts"
    ],
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "exclude": [
        "dist_prod"
    ]
}

The --showConfig option instructs the TypeScript compiler to only dump the interpreted configuration settings while --project or simply -p specifies the tsconfig file path to use for the compilation.

You can ignore the files and exclude fields since they are computed by the TypeScript compiler from include and outDir, but it clearly shows that objects are merged deeply (see the compilerOptions object) while arrays are overridden (see the modules/* array).

listFiles

The listFiles compiler-option indicates the TypeScript compiler to dump the list of source files that were part of the compilation.

$ tsc
/home/node_modules/typescript/lib/lib.d.ts
/home/node_modules/typescript/lib/lib.es5.d.ts
/home/node_modules/typescript/lib/lib.dom.d.ts
/home/node_modules/typescript/lib/lib.webworker.importscripts.d.ts
/home/node_modules/typescript/lib/lib.scripthost.d.ts
/projects/samples/a.ts
/projects/samples/src/b.ts
/projects/samples/src/c.ts
/projects/samples/src/lib/d.ts
/projects/samples/src/lib/e.ts
/projects/samples/node_modules/@types/json5/index.d.ts

listEmittedFiles

The listEmittedFiles compiler-option behaves just like listFiles option but instead it prints the files that were generated in the compilation output.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "sourceMap": true,
        "listEmittedFiles": true
    }
}

Using the above tsconfig.json, the TypeScript compiler prints the list of files that were generated at the end of the compilation.

$ tsc
TSFILE: /projects/samples/dist/a.js
TSFILE: /projects/samples/dist/a.js.map
TSFILE: /projects/samples/dist/src/b.js
TSFILE: /projects/samples/dist/src/b.js.map
TSFILE: /projects/samples/dist/src/c.js
TSFILE: /projects/samples/dist/src/c.js.map
TSFILE: /projects/samples/dist/src/lib/d.js
TSFILE: /projects/samples/dist/src/lib/d.js.map
TSFILE: /projects/samples/dist/src/lib/e.js
TSFILE: /projects/samples/dist/src/lib/e.js.map

noEmit

The noEmit compiler-option prevent the generation of any build files such as JavaScript files (.js), source-map files (.map), and declaration files (.d.ts). This is option is used by tools such as babel or Webpack.

This option is quite useful when you want to only statically type analyze your source code (using the TypeScript compiler) but do not want to generate any build files. For example, you can use this with the listFiles option to display the files without creating a build.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "listFiles": true,
        "noEmit": true
    }
}

noEmitOnError

By default, the TypeScript compiler emits JavaScript files (.js), source-map files (.map), and declaration files (.d.ts) even if it encounters some compilation errors. This behavior is kept as default to allow continuous code generation in watch mode.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "noEmitOnError": true
    }
}

If you want to disable this behavior, set the noEmitOnError compiler-option to true. In this case, the TypeScript compiler won’t emit any build files unless all compilation errors are resolved.

emitDeclarationOnly

The emitDeclarationOnly compiler-options instructs the TypeScript compiler to only emit declaration files (.d.ts). This can be useful when you only want to ship type declarations for your old JavaScript project.

// tsconfig.json
{
    "include": [
        "a.ts",
        "src/**/*.ts"
    ],
    "compilerOptions": {
        "outDir": "dist",
        "declaration": true,
        "emitDeclarationOnly": true
    }
}

For the above tsconfig.json, the TypeScript compiler will only output .d.ts files for each source .ts files in the dist directory (unless a directory is specified using declarationDir option). The declaration option also needs to be true since TypeScript doesn’t emit declarations files by default.

/projects/sample/dist/
├── a.d.ts
└── src/
   ├── b.d.ts
   ├── c.d.ts
   └── lib/
      ├── d.d.ts
      └── e.d.ts

You do not necessarily need a tsconfig.json file to configure TypeScript compilation. The tsc command also provides equivalent flags to control the TypeScript compiler-options. Please follow the next lesson (coming soon) for more details.

#typescript #compilation-process