TypeScript Learning Adventures: A Tale of Love and Hate - Compiler settings
In this article, we'll take a closer look at the TypeScript Compiler. Thus far, we always used it by running tsc command and then pointing at a file that we wanna compile.
In this article, we'll take a closer look at the TypeScript Compiler. Thus far, we always used it by running tsc
command and then pointing at a file that we wanna compile.
Using this command is not feasible for bigger projects where you have many files. Or where you don't want to run this command after every change you wanna see.
There are also some interesting things you can configure in the compilation process. The result will actually change what is compiled and how it is compiled.
For that, we can use this project startup template here.
Watch mode
If you don't want to rerun tsc
command for every single change, you can use TypeScript watch mode.
With this, we can tell TypeScript to watch a file. And whenever that file changes, TypeScript will recompile it. To do that, we can still run the same tsc
command, but now we add --watch
at the or -w
:
tsc main.ts --watch
If we do that, then we are in watch mode on that file. Now, whenever we change anything in there, and save it, it will recompile the file.
This also means that if we would do anything which is not allowed, we see the compilation error down there. For example, reassigning to a constant, or using a wrong type.
So, watch mode is already a big improvement. The downside is that we still have to target a specific file here. At the moment, this is the only file we're working with, so it's ok. But in bigger projects, that's usually not the case.
How to compile the entire project?
So as I mentioned before, watch mode is a great start but what if we have more than one TypeScript file?
For that, we can create one more file in the project, a second.ts
file.
It would be nice if we could enter some general watch mode. The watch mode without pointing at a single file and it watches our entire project folder. And of course, it recompiles any TypeScript file that might change into JavaScript.
Well, turns out that this is possible.
For that, we need to tell TypeScript that this here is our project and that TypeScript should manage it. We do that by running this command:
tsc --init
And we run this command only once and never again.
So I'm not pointing at a specific file here, I run tsc
and then --init
here and again, this is only required once.
It will initialize this project in which you run this command as a TypeScript project. It will tell TypeScript that all our typescript files are in the current folder.
Thus it is important that before you run this command you navigated to the right folder. The result of this command will be tsconfig.json
file, which looks like this:
{
"compilerOptions": {
...
}
}
This tells TypeScript that all files and sub-folders inside the current folder should be managed by TypeScript.
Now, if we look into the tsconfig.json
file, we see there are a bunch of options. Most of them are commented out. They're there so that you see that you could enable them. Also, you've got a short explanation as well but we don't have to worry about those right now.
We can now run tsc
like this without pointing at a specific file.
This will tell TypeScript to go ahead and compile all TypeScript files. So all .ts files it can find in this project. As a result, we got the second.js
file and this main.js
file:
And of course, this can also be combined with watch mode. You can run tsc -w
or --watch
as I showed before and this will enter watch mode for all TypeScript files.
So whenever I change one of the files and I save it, it will recompile.
How to exclude/include files in the project?
Let's have a look at the tsconfig.json
file as this is a crucial file for managing this project. It tells TypeScript how it should compile these files.
Before we dive into the compilerOptions
let's scroll down to the place before the closing curly brace. As the name suggests compilerOptions
allow us to configure how the compiler behaves.
After this nested closing curly brace, we can add some commands which don't affect the compilation step behavior. Instead, it tweaks how the compiler works with this project. Because there, for example, you can set a exclude option.
Now if you add exclude here:
{
"compilerOptions": {
...
}
"exclude": ["second.ts"]
}
That is an array. You can here enter paths to files that shouldn't be included in compilation when you run the tsc
command.
So for example, here we could say we want to exclude second.ts
from the compilation. If we first delete second.js
and rerun tsc
, we can see if it is recreated. We now run tsc
command and you see no second.js
file is created. The reason for that is that we're excluding that file.
You can also work with wildcards. For example, if you had a file that's named second.dev.ts
and you don't wanna compile that. You could say all files that end with dev.ts
should not be compiled:
{
"compilerOptions": {
...
}
"exclude": ["*dev.ts"]
}
You can do that by adding an asterisk here, which is a wildcard. Now TypeScript will ignore any files that have .dev.ts
at the end of the file name.
You could also add something like this:
{
"compilerOptions": {
...
}
"exclude": ["**/*"]
}
That would mean any file with that pattern in any folder will be ignored.
So these are things you can set up here. Usually, the only thing I want to set up here is to exclude node_modules
. And the idea here is that I don't want to compile any TypeScript files inside of the node_modules
folder.
The node_modules
is a folder that holds all the dependencies we have in package.json
file. And the dependencies of these dependencies.
So, these are third-party libraries we're importing, which we don't wanna touch. If any of these libraries should ship some TypeScript code, then we don't want to compile it. It will slow down our compilation process, and in the worst case, it might even break our project.
So, it's quite common to exclude node_modules
here:
{
"compilerOptions": {
...
}
"exclude": ["node_modules"]
}
As a side note, if you don't specify the exclude option at all, node_modules
is excluded as a default setting. So you don't need to add this option here, this would be the default. But I want to show that exclusion exists and how you could use it. If the only thing you want to exclude is node_modules
, you don't have to add the exclude
property at all.
Now besides exclude
option, we also have the include
option. Include option allows you to do the opposite. It allows you to tell TypeScript which files you want to include in the compilation process. Anything that's not listed there will not be compiled.
So if I point at main.ts
here, and we rerun tsc
, we will get no second.js
file.
Why?
Because second.js
is not included in the include
option. And as I said, if we do set the include
key, then we have to include everything we want to compile.
Now you also have a files
option, which allows you to point at the individual files. So it's a bit like include
with the difference that here you can't specify whole folders. Instead, you specify the individual files you want to compile.
That might be an option for smaller projects where you know you will only work with a couple of files. And for some reason, you got a couple of other TypeScript files which you don't want to touch.
In reality, you might not need that setting that often though.
How to set compilation target?
Now that we know how we can manage our files with the compiler, let's dive into the compiler options.
This allows us to control how our TypeScript code is compiled. So not only which files, but also how the files which are getting compiled are treated by TypeScript.
And there you see we have a bunch of options. You got short explanations next to these options. Some explanations are a bit confusing. Others are quite clear:
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
I will say that a lot of these options, most of these options will not matter in most projects. So, you'll not set all these options. You can ignore a lot of these options.
I will pick up on the important options throughout this article. Because some options only make sense when we use them for specific features.
Let's start with the target
option.
As you see, this actually is set by default. It's not commented out.
With this option, you can tell TypeScript which target JavaScript version you want to compile the code. It compiles the code to JavaScript that runs in a certain set of browsers. And you define which browsers support the compiled code by setting the target.
The default target here in this project is set up as ES5
, which means all types of code is compiled down to that version.
We can actually see that.
If we run tsc
here to compile all files, we see in app.ts I'm using let
and const
, but in main.js
, we see var
. And that happens because we got a target of ES5
and in that version, we don't have let
and const
.
So the good thing here is that we can use TypeScript to generate code that works in older browsers as well.
The more recent JavaScript version you pick as a target, the more concise your generated code is. TypeScript has to compile less code. Or it has to work around non-existing features in fewer situations. And thus, the compiled code is more concise and shorter.
So that's what the target
option is doing.
Typescript core libs
Let's now explain a couple of other compilerOptions
properties and what they do.
The lib option allows you to specify which default objects and features you want to use in your TypeScript code.
With that I mean things like working with the DOM.
Let's say in index.html we have a button and on this button, we say "Hello world":
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="main.js"></script>
<button type="button" id="myButton">Click me</button>
</body>
</html>
Now, in app.ts
we can select this button. We can get access to this button with document.querySelector
. For example, selecting the first button we find. Now if we do that then this works. We get no types of error here:
function handleClick() {
console.log("Hello world!");
}
const button = document.getElementById("myButton");
button.addEventListener("click", handleClick);
Now, shouldn't TypeScript complain that the document is unknown here? How does it know that we have such a document, constant or variable available?
How does it know that even if we have that available that it holds an object which has our querySelector
method? How does it know that button is something which has the addEventListener
method?
How does TypeScript know all that?
Now you might say, of course, it knows because in vanilla JavaScript this would be valid code. But keep in mind that when you write TypeScript code, you don't write it for the browser.
You could be writing your Node.js application with TypeScript. And there indeed this would not work.
So, the reason why this works is this lib option. As you see it's not even set here, but if it isn't set then some default rules apply.
The defaults depend on the JavaScript target that you set in the same file. And for es6 it, by default, includes all the features that are globally available in ES6.
For example, the Map
object is available in ES6. Thus it wouldn't complain if you use Map
. So it assumes all the ES6
features which are available globally in JavaScript, that they are available in TypeScript as well.
And besides, it assumes that all DOM APIs are available.
So, long story short, if the lib option is not set some defaults are assumed and these are typically the defaults you need to have TypeScript run in the browser.
So, all the DOM APIs are gone.
If we enable this property and recompile everything we definitely get an error. Because now that it's commented out we don't have the default settings anymore.
Instead, we now say, "Hey, please include some default libraries". Some default type definitions we will give you in this array.
So if we set the default, well then TypeScript, of course, adheres to what we setting here. And here, for example, it doesn't know the document. It doesn't even know the console here.
If you hit control space, and here you get auto-completion. For example, there we could add dom. So if you want the default behavior, you can set the following:
{
"compilerOptions": {
"lib": ["DOM","DOM.Iterable","ES6", "ScriptHost"]
}
}
So, if you comment this in and set it up like this, you have exactly the same behavior as if you don't specify lib at all.
allowJS, checkJS and jsx options
With allowJs
and checkJs
you can always include JavaScript files in the compilation.
With allowJs
a JavaScript file will be checked by TypeScript. So even if doesn't end with .ts, TypeScript will analyze it with checkJs
option enabled. It will not compile it but it will still check the syntax in there and report potential errors.
This could be nice if you don't wanna use TypeScript but you wanna take advantage of some of its features.
jsx
option is only for those who are working with React and must write JSX code.
declaration
and declarationMap
options are not as important as .dts files are advanced concepts. Those matters to you if you're shipping your project as a library to other people. You need a manifest file that describes all the types you have in your project, that's such a .dts file.
Source Maps
sourceMap
helps us with debugging and development.
So if we compile everything and go to the web browser, to the sources tab in developer tools and there we find our JavaScript files
Now we can dive into these files and debug them.
That's good but what if we had more complex TypeScript code and we want to debug our TypeScript code? Not the compiled JavaScript code?
In other words, it would be nice if we would see the TypeScript files here and not the JavaScript files.
With the sourceMap option, you can get there.
If you set this sourceMap
option to true and you run the tsc
command again then you see we got these .map
files generated as well:
{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":";AAAA,SAAS,WAAW;IAClB,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;AAEnD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC"}
if we look at them they're pretty strange files. What they do is they act as a bridge. These files are understood by modern browsers. Because of that browser developer tools can connect the JavaScript files to the input TypeScript files.
So with these files generated here, you see in the sources tab in the web browser we now do not have our JavaScript files. We also see our TypeScript files there.
And we can even place break points in the TypeScript files. which is of course super, super convenient.
That takes our debugging process to the next level. We can work in our TypeScript files, instead of the JavaScript files.
rootDir and outDir
The bigger your project gets, the more you might want to organize your files.
You don't want to have your files lie around here in your root-level project folder. Instead, what you often will see in projects is that you have a src
folder, and you have a dist
folder.
So, the dist
folder has the job of holding all the output, so all the JavaScript files, let's say. And the src
folder might hold all our TypeScript files. So we can move the TypeScript files into the src
folder:
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
}
}
If I now delete the JavaScript files, we have a problem if we compile everything. These TypeScript files are compiled because the TypeScript compiler does look into sub-folders. But the output sits next to our input files.
And that's something we can control with the outDir
, for example.
If we set outDir
, we can tell the types with the compiler where the created file should be stored. We could set this to dist
folder.
Then if you run tsc you will see that the JavaScript files are not placed in the src
folder but in the dist
folder.
Now the good thing is that if we had a sub-folder here, that folder structure will be replicated in the dist
folder. So that the structure you set up there is kept.
Errors on compilation
One interesting property is the noEmitOnError
option. You can set this to true or false and the default is false.
Now what does this do? If we set it to false, let me show you where this might be a problem. It is a problem if we introduce an error or it can be a problem.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="main.js"></script>
<button type="button" id="myButton">Click me</button>
</body>
</html>
Let's say here, I do have my button and I remove this exclamation mark. Now the problem here is that TypeScript does not know that we have a button here:
const button = document.getElementById("myButton");
button.addEventListener("click", handleClick); // button is possibly null
After all, when querying for a button we might not get one. If there is no element in the DOMs that's satisfying this selector then this will return now.
And that's what TypeScript complains about.
Here we access something on a potential null object and that's not good. Now that's an error we have here. If we compile our code, we also get this error here in the console. Nonetheless, the js file is created.
So even if I delete the app.js
file it will be recreated. So even if we have an error, TypeScript creates a JavaScript file.
This might or might not be wanted. Maybe you have an error in your TypeScript file and you don't really know how to work around it. But you know it will not be a problem in the final app.
But still, we know that this will work on our page here. So we might be fine with compiling this despite having an error.
But, of course, you should aim for error-free projects. Rather learn how you can work around these issues than ignore them.
Nonetheless, you could set this to false. Or not set it at all, because false is the default, if you are fine with generating JavaScript files if you have an error.
If you set this to true, what will happen is that problematic files will not be generated. If I now rerun this, you see, nothing is generated actually. Even the second.ts file is not output there.
And the reason for that is that we have an error in the file. And if any file fails to compile no files will be omitted. So here, we have to make sure we fix this error before we then can get TypeScript to again compile files for us.
Thus, it is an option I like to set. Because I'm not interested in getting JavaScript files if I still have errors in my TypeScript files.
Conclusion
In conclusion, the TypeScript compiler is a powerful tool that enhances JavaScript development by providing features such as:
- watch mode for automatic recompilation,
- compiling multiple files for managing complex projects,
- including/excluding files for better control,
- setting compilation targets for platform compatibility,
- core libraries for additional functionality,
- source maps for effective debugging,
- and options like
rootDir
andoutDir
for organizing code structure.
There are also many other options for strict compilation and code quality options like noUnusedParameters
, noFallthroughCasesInSwitch
, allowUnreachableCode
but they are self-explanatory.
With detailed error messages, the TypeScript compiler helps catch and resolve issues early in the development process, making it an essential tool for building robust and maintainable TypeScript applications.
Comments ()