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.

TypeScript Learning Adventures: A Tale of Love and Hate - Compiler settings
Photo by Adi Goldstein / Unsplash

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.

Binoculars
Photo by Chase Clark / Unsplash

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.

Success hitting target aim goal achievement concept background - three darts in bull's eye close up. red three darts arrows in the target center business goal concept
Photo by Afif Ramdhasuma / Unsplash

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.

Perfectly Ripe Avocado
Photo by Kelly Sikkema / Unsplash

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

Instagram - @andrewtneel | Donations - paypal.me/AndrewNeel
Photo by Andrew Neel / Unsplash

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.

I don’t know who dropped this in Leeds city centre but I feel their disappointment.
Photo by Sarah Kilian / Unsplash

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 and outDir 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.