Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Exploring advanced compiler options in TypeScript

5 min read 1661

Exploring Advanced Compiler Options in TypeScript

Introduction

The tsconfig.json file specifies compilation options used by the TypeScript compiler, which applies checks to our code and determines whether any of these checks fail. These options include which version of JavaScript that our TypeScript code will be compiled to, what the output directory should be, and whether or not to allow JavaScript source files within the project directory.

In this article, we will take an in-depth look at some advanced compiler options and a few other options that help us find potential problems in our TypeScript codebase. A deep understanding of these compiler options and what causes code to fail the strict checking rules will help us avoid common mistakes when building TypeScript applications.

Specifically, this article will cover the following options:

Nested tsconfig.json files

The TypeScript compiler can reference a tsconfig.json file in another directory when compiling code in the current directory.

This feature is handy if we would like to reference a compiler option when running tsc within a specific directory. The tsconfig.json file uses the "references" option for this purpose.

As an example of this nested configuration, consider the following source tree:

├── dist
└── src
    ├── tsconfig.json
    ├── backend
    │   ├── index.ts
    │   └── tsconfig.json
    └── frontend
        ├── index.ts
        └── tsconfig.json 

Here, we have a tsconfig.json file in the project’s src directory, as well as two subdirectories named frontend and backend. Both subdirectories contain a tsconfig.json file and a TypeScript file named index.ts.

The tsconfig.json file in the project’s src directory is as follows:

{
    "compilerOptions": {
      "target": "es5", 
      "module": "commonjs", 
      "rootDir": ".",
      "outDir": "../dist/",
    },
    "files": [],
    "references": [
      { "path": "./backend" },
      { "path": "./frontend" }
    ]
  }

Here, we have specified the outDir property to generate all JavaScript output into the dist directory, followed by configuring reference paths for both subdirectories.

The whole project can be compiled with the following command:

We made a custom demo for .
No really. Click here to check it out.

tsc --build src

Let’s take a look at the tsconfig.json file in the backend directory, as follows:

{
    "compilerOptions": {
      "rootDir": ".",
      "outDir": "../../dist/backend",
    }
  }

Here, we have specified the outDir property to generate all JavaScript output into the dist directory.

This means that the TypeScript compiler will output all the JavaScript files in this directory to the dist directory, which is two directory levels up.

The frontend subdirectory can be built independently using the following command:

tsc --build src/frontend

Let’s take a look at the tsconfig.json file in the backend directory:

{
    "compilerOptions": {
      "rootDir": ".",
      "outDir": "../../dist/frontend",
    },
    "references": [
      { "path": "../backend" }
      "composite": true
    ]
  }

Similarly, we have specified the outDir property to generate all JavaScript output in this directory to the dist directory, which is two directory levels up, followed by configuring the reference path for the backend subdirectory.

Take note of this info from the TypeScript docs: “The referenced projects must have the new composite setting enabled. This setting is needed to ensure TypeScript can quickly determine where to find the outputs of the referenced project.”

Additionally, the backend subdirectory can be built independently using the following command:

tsc --build src/backend

strictPropertyInitialization

When enabled, the strictPropertyInitialization compiler option ensures that all properties within a class are initialized correctly.

Let’s consider the following class definition:

class NoInitProperties { 
  a: number; 
  b: string; 
}

Here, we have a class named NoInitProperties, which has two properties, named a of type number and b of type string. The above code will generate the following errors:

error TS2564: Property 'a' has no initializer and is not definitely assigned in the constructor 
error TS2564: Property 'b' has no initializer and is not definitely assigned in the constructor

These errors are being generated because both a and b properties of the class have not been initialized.

Solving strictPropertyInitialization issues

There are four ways that we can fix this code.

The first method is commonly used for fixing these errors and it uses a constructor:

class NoInitProperties { 
    a: number; 
    b: string; 
    constructor(b: string) { 
      this.a = 5; 
      this.b = b; 
    } 
}

Here, we have defined a constructor function with parameter b of type string. Within the constructor, we’ve assigned the value of the b parameter to the internal b property. Also, we’ve assigned the string value "letter" to the property named a. With this constructor in place,
the error will be fixed because both properties are now properly initialized.

The second method is to use a type union:

class NoInitProperties { 
    a: number | undefined; 
    b: string | undefined; 
}

Here, the union type is used to add the undefined type to both the a and b properties. With this, the compiler knows that we are aware that these properties could be undefined, which will allow us to handle the consequences ourselves.

The third method that we can use to fix these errors is to use the definite assignment assertion operator:

class NoInitProperties { 
    a!: number; 
    b!: string; 
}

The ! operator added after each property tells the compiler that we are aware that these properties have not been initialized.

The fourth method to fix these errors is to assign a value to each of these properties:

class NoInitProperties { 
    a: number = 5; 
    b: string = "letter"; 
}

Here, we have assigned the numeric value of 5 to the a property and the string value of "letter" to the b property.

noImplicitThis

The noImplicitThis compiler option will ensure that the this keyword is accessed correctly or else the compiler will throw an error indicating incorrect access to this.
Let’s consider the following code:

class NoImplicitThisClass { 
    name: string = "Tom"; 
    logToConsole() { 
        let callback = function () { 
            console.log(`this.name : ${this.name}`); 
        } 
      setTimeout(callback, 1000); 
    } 
}

Here, we have a class named noImplicitThisClass that has a name property initialized with a string value of Tom.

Also, the class defines a function named logToConsole that, when called, triggers the function callback after two seconds. This class is used as follows:

let instanceOfClass = new NoImplicitThisClass(); 
instanceOfClass.logToConsole();

Here, we’ve created a variable named instanceOfClass to hold an instance of the NoImplicitThisClass, and calling the logToConsole function will output the following:

this.name : undefined

Here is what happened: the this property does not reference the NoImplicitThisClass class. This is due to the scoping of the this property within JavaScript. In JavaScript, the this scope in methods is not bound to any reference by default.

If the noImplicitThis compiler option is turned on, the compiler will generate the following error:

error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation

Here, the compiler notifies us that our reference to this.name within the callback function is not referencing the this property of the NoImplicitThisClass class.

Solving noImplicitThis issues

This error can be resolved by passing the this property into the callback function as follows:

let callback = function (_this) { 
    console.log(`this.name : ${_this.name}`); 
} 
setTimeout(callback, 2000, this);

Here, we have added a parameter named _this to the callback function and then passed the value of this into the setTimeout call.

Another common way to resolve this error is to use an arrow function. This is very common in React:

let callback = () => { 
    console.log(`this.name : ${this.name}`); 
} 
setTimeout(callback, 2000)

Here, we have replaced the function keyword with the arrow function syntax.

Both solutions will have the following result:

this.name : Tom

I use the arrow function frequently to handle this issues in my React projects, and I’d recommend using the arrow function since it’s a lot cleaner.

noImplicitReturns

The noImplicitReturns compiler option will ensure that every function declared with a return value must return the value as defined in the function.
Let’s consider the following code:

function fetchUsernameById(id: number): string { 
    if (id === 2) return "Sam"; 
} 
console.log(`fetchUsernameById(4) : ${fetchUsernameById(4)}`)

Here, fetchUsernameById has a parameter id of type number and returns a string value. The function checks if the value passed in as an argument is equal to 2. If it is, it returns the string value Sam. However, if the value of the argument is not equal to 2, nothing is returned.

This will be the output of running this code:

fetchUsernameById(4) : undefined

Here, we can see that the fetchUsernameById function returns undefined for any value of the argument that is not equal to 2.
If the noImplicitReturns compiler option set to true, the compiler will generate an error:

error TS7030: Not all code paths return a value.

Solving noImplicitReturns issues

This error can be resolved by returning a string value for ids that are not equal to 2:

function fetchUsernameById(id: number): string { 
    if (id === 2) 
      return "Sam"; 
    return "No user with such id"
}

Here, we have added a return statement at the end of the function that will return the string “No user with such id” whenever the function is called with an argument of a value not equal to 2.

strictNullChecks

The strictNullChecks compiler option is used to find instances in our code where the value of a variable could be null or undefined at the time of usage.
Let’s consider the following code:

let a: number; 
let b = a;

The above code will generate the following error:

error TS2454: Variable 'a' is used before being assigned

This error tells us that the value of the variable a is used when it has not yet been assigned a value.
Technically, the value of a could be undefined.

Solving strictNullChecks issues

This error can be resolved by ensuring that the variable a is assigned a value before being used:

let a: number = 4; 
let b = a;

Here, we have simply assigned the value of 4 to the variable named a, and this will resolve the error.

Another way to fix this error is using the union type to inform the compiler that we are aware that the variable may be unassigned at the time of usage, and we will handle the consequences ourselves:

let a: number | undefined; 
let b = a;

Conclusion

In this article, we took a look at the various advanced compiler options available for configuring our TypeScript compiler. We also have seen the error messages associated with each compiler option and how to resolve them.

Check out Typescript official documentation for more compiler options.

Writing a lot of TypeScript? Signup for our upcoming TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Join us on Sept 30th at 2pm EDT for a deep dive on some new features of TypeScript 4.4.

Save your seat.

Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Leave a Reply