![](/content/typescript/compilation-process/hero.jpg)
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 thetsconfig.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
orES6
modules can not be bundle has to do with the file import locations. If an import statement insidex.js
(compiled file) looks likerequire('dir/y')
, this means whenever this code runs, they.js
is imported fromdir
directory relative tox.js
. When these files are bundled into a single file, it becomes unportable sincex.js
andy.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 thetarget
option, if you do not want TypeScript to include any libraries implicitly, setnoLib
compiler-option totrue
.
● 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
ortypings
property of thepackage.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 animport
nor anexport
statement. This clarifies that whennoEmitHelpers
istrue
, 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 supportimport * as
syntax. You can useimportHelpers
option to import these helpers fromtslib
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.