DEV Community

Cover image for How To Configure tsconfig.json: TypeScript Strict options
Max Kovalevsky
Max Kovalevsky

Posted on • Edited on • Originally published at byte.ski

How To Configure tsconfig.json: TypeScript Strict options

This post is part of series of posts about TypeScript called Grokking TypeScript. It will guide you from scratch to writing full TypeScript applications on Back End and Front End. The series is available as PDF eBook for free to everyone.

Grokking TypeScript eBook (pdf)

TypeScript is not just a superset of JavaScript with static types. It is also a quite configurable tool that can be used for different types of projects. One parameter or group of parameters that can be configured is strict. If you are not familiar with strict mode and why you should use it for a new project then check out the post What Is Strict Mode In TypeScript, Why And When You Should Use It?. In this post I focus more on a practical side of this topic.

TypeScript's strict mode parameter can be configurated as several individual parameters for each specific case of type checking. So, basically, if you set the parameter strict to true in tsconfig.json it means that all these strict options are set to true.

List of strict options:

  • useUnknownInCatchVariables (new)
  • noImplicitAny
  • strictNullChecks
  • strictFunctionTypes
  • strictBindCallApply
  • strictPropertyInitialization
  • noImplicitThis
  • alwaysStrict

Let's explore each strict option in practice.

TypeScript Strict options in tsconfig.json: useUnknownInCatchVariables

This option was introduced in TypeScript 4.4.

The problem is that when we use construction try catch the type of variable error in catch is any:

Screenshot of an example in VSCode 1

It increases the potential risk of errors and application malfunction. The option useUnknownInCatchVariables solves this problem.

If you set option useUnknownInCatchVariables to true then variable error in every try catch in your code base will have type unknown:

{
  "compilerOptions": {
    // ...
    "useUnknownInCatchVariables": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Screenshot of an example in VSCode 1

You can also use type Error for error variable:

try {
    // some code
}
catch (e) {
    if (e instanceof Error) {
        console.error(e.message);
    }
}
Enter fullscreen mode Exit fullscreen mode

TypeScript Strict options in tsconfig.json: noImplicitAny

Let's start with option noImplicitAny.

In the main.ts file (or whatever file you want) let's create a simple function:

function printData(data) {
  console.log(data);
}
Enter fullscreen mode Exit fullscreen mode

If you run tsc command you will see that TypeScript successfully compiles the code because there is no error.

Now, set the options in configuration file tsconfig.json in your project:

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}
Enter fullscreen mode Exit fullscreen mode

If you're writing your code in an editor like Visual Studio Code or some IDE you probably already see that something is wrong with parameter data in the function. Let's run TypeScript compiler tsc and see what it will tell us.

TypeScript compiler will print something like this:

error TS7006: Parameter 'data' implicitly has an 'any' type.

4 function printData(data) {
                     ~~~~
Found 1 error.
Enter fullscreen mode Exit fullscreen mode

So, if you set the option noImplicitAny to true, TypeScript won't allow us to write functions with parameters without types of parameters. The thing is that TypeScript doesn't know what type of the parameter data is and it doesn't infer because there is no information in the code about that value should be there.

You need to set some type to avoid this TypeScript error. For example, I'll specify type string for the data:

function printData(data: string) {
  console.log(data);
}
Enter fullscreen mode Exit fullscreen mode

Also, if your parameter is not required, you can specify the default value of the parameter. And there is the thing: if you set the default value of the parameter then you won't need to specify the type. In that case, TypeScript will understand what type of the parameter is by Type inference.

An example. The default value of the parameter is empty string so type of the parameter is string:

function printData(data = "") {
  console.log(data);
}
Enter fullscreen mode Exit fullscreen mode

TypeScript Strict options in tsconfig.json: Why Should noImplicitAny Be Enabled?

By setting the option noImplicitAny to true, TypeScript forces you to write safer code. How? The problem with ignorance of the type of the parameter is that you can manipulate the value in the code by methods that can't work with this value. For example, inside the function printData you can use method .toLowerCase that works with type string. Your colleague (or even you!) can use the function printData somewhere in the future. Because you don't know what the type of the parameter data is, you probably can put the number value to this parameter.

function printData(data) {
  console.log(data.toLowerCase());
}

async function main() {
  printData(10);
}

main();
Enter fullscreen mode Exit fullscreen mode

The code above will successfully be compiled by tsc because there are no errors from the TypeScript perspective. But when you will run the program in the Web browser or by Node as in our case, you will see that program falls:

node dist/main.js
/ts-node-sample/dist/main.js:13
    console.log(data.toLowerCase());
                     ^
TypeError: data.toLowerCase is not a function
Enter fullscreen mode Exit fullscreen mode

You can avoid this error before executing the code by specifying the type of the parameter. The TypeScript's option noImplicitAny won't allow you to escape from specifying the type in the new code.

TypeScript Strict options in tsconfig.json: strictNullChecks

Source code of this example is available on GitHub

This parameter obligates us to make a check of the variable existing. For example, let's say we have an array of some object. This data is available in a code of app from JSON file:

src/inventory.json

[
    {
        "id": "1",
        "name": "sword",
        "level": "10",
        "icon": "üó°"
    },
    {
        "id": "2",
        "name": "bow",
        "level": "7",
        "icon": "üèπ"
    },
    {
        "id": "3",
        "name": "shield",
        "level": "5",
        "icon": "üõ°"
    }
]
Enter fullscreen mode Exit fullscreen mode

In some modules, we have a code where this JSON file is imported and used as a database. The app is simple: it asks the user to type the name of the item from inventory and then if this item exists the program will print information about it.

src/main.ts

import { createQuestioner } from "./createQuestioner";
import { greeting } from "./greeting";

import inventory from "./inventory.json";

async function main() {
  try {
    const questioner = createQuestioner();
    const username = await questioner.ask("Type your username: ");

    greeting(username);

    const itemName = await questioner.ask(
      "Type the name of the inventory item: "
    );

    const foundItem = inventory.find((item) => item.name === itemName);

    console.log(
      `You've chosen an item: ${foundItem.icon} ${foundItem.name} (lvl ${foundItem.level})`
    );

    questioner.finishUp();
  } catch (e) {
    console.error(e);
  }
}

main();
Enter fullscreen mode Exit fullscreen mode

If you run this program by npm run dev, type any name and one of three item's names (sword, bow, shield) the program will run as it should. The problems begin when you type the name of the item that does not exist in the inventory. If you try this, you'll see something like this:

‚Äö√π√ò npm run dev

> tsc-intro@1.0.0 dev
> tsc && node dist/main.js

Type your username: byte
Hello, @byte!
Type the name of the inventory item: spear
TypeError: Cannot read property 'icon' of undefine
Enter fullscreen mode Exit fullscreen mode

All we need to do to fix this problem is to add the code that checks the variable existing before using it for printing the result. But the point is that TypeScript should highlight that we need to fix the potential problem. To do it just set option strictNullChecks to true:

tsconfig.json

{
  "compilerOptions": {
    // ...
    "strictNullChecks": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's run npm run dev and see that happens:

src/main.ts:20:33 - error TS2532: Object is possibly 'undefined'.

20       `You've chosen an item: ${foundItem.icon} ${foundItem.name} (lvl ${foundItem.level})`
                                   ~~~~~~~~~

src/main.ts:20:51 - error TS2532: Object is possibly 'undefined'.

20       `You've chosen an item: ${foundItem.icon} ${foundItem.name} (lvl ${foundItem.level})`
                                                     ~~~~~~~~~

src/main.ts:20:74 - error TS2532: Object is possibly 'undefined'.

20       `You've chosen an item: ${foundItem.icon} ${foundItem.name} (lvl ${foundItem.level})`
                                                                            ~~~~~~~~~

Found 3 errors
Enter fullscreen mode Exit fullscreen mode

Great! Now we have information about where the problem is. Just add checking the variable foundItem:

async function main() {
  try {
    const questioner = createQuestioner();
    const username = await questioner.ask("Type your username: ");

    greeting(username);

    const itemName = await questioner.ask(
      "Type the name of the inventory item: "
    );

    const foundItem = inventory.find((item) => item.name === itemName);

    if (!foundItem) {
      console.log(`There is no item with name '${itemName}' in the inventory.`);
      questioner.finishUp();
      return;
    }

    console.log(
      `You've chosen an item: ${foundItem.icon} ${foundItem.name} (lvl ${foundItem.level})`
    );

    questioner.finishUp();
  } catch (e) {
    console.error(e);
  }
}
Enter fullscreen mode Exit fullscreen mode

TypeScript Strict options in tsconfig.json: strictNullChecks and Exclamation mark

You can also use "!" in such a case when you are sure that found item or element exist. Let's see an example:

async function main() {
  try {
    const questioner = createQuestioner();
    const username = await questioner.ask("Type your username: ");

    greeting(username);

    const listOfItems = inventory
      .map(
        (item) => `${item.id}) ${item.icon} ${item.name} (lvl ${item.level})`
      )
      .join("\n");

    const option = await questioner.ask(
      `\n${listOfItems}\n\nChoose the item (type the number): `
    );

    const itemsIds = inventory.map((item) => item.id);

    if (!itemsIds.includes(option)) {
      console.log(`There is no item with option number ${option}.`);
      questioner.finishUp();
      return;
    }

    const foundItem = inventory.find((item) => item.id === option);

    console.log(
      `You've chosen an item: ${foundItem.icon} ${foundItem.name} (lvl ${foundItem.level})`
    );

    questioner.finishUp();
  } catch (e) {
    console.error(e);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this case, a user is not typing the name of the inventory item but type an option number offered by the app. Because the code checks that the user typed option number that surely exists (the line if (!itemsIds.includes(option)) {) we don't need to manually check that variable foundItem has data inside. But TypeScript will tell us that we need to check this variable because Object is possibly 'undefined'. To avoid this highlight we can use exclamation mark:

console.log(
  `You've chosen an item: ${foundItem!.icon} ${foundItem!.name} (lvl ${
    foundItem!.level
  })`
);
Enter fullscreen mode Exit fullscreen mode

It tells TypeScript that we are totally sure that foundItem is not undefined or null. After that you can run the app it will work correctly.

I recommend not use **exclamation mark* very often because it can expand the count of potential mistakes in the future. Use it only in case when you are totally sure that some data exists.*

TypeScript Strict options in tsconfig.json: strictBindCallApply

Source code of this example is available on GitHub

The next option is not so useful nowadays since we don't need to use bind() and related methods much often in modern JavaScript. But anyway, if you need to use bind(), call(), or apply() then this option might be useful for you.

The example is unusual but you may come across this in existing projects with an old version of ECMAScript (where arrow functions are not available or their support is disabled for some reason). This function creates an object of a non-player character. You can start the dialog with this character (in our example it starts automatically after running the app) but the character is busy right now so it answers later (after 2 sec):

import { Questioner } from "./createQuestioner";

export function createMerchant(name: string, questioner: Questioner) {
  async function greeting(caller: { name: string; level: number }) {
    console.log("\nDid you complete the quest? \n 1) yes \n 2) no");
    const answer = await questioner.ask("\nYour answer: ");

    if (answer === "1") {
      console.log(`\nExcellent! Now your level is: ${caller.level + 1}`);
    } else {
      console.log("\nSee ya later");
    }

    questioner.finishUp();
  }

  const character = {
    name,
    startDialog: function (caller: { name: string; level: string }) {
      console.log("[This character is busy now]");
      setTimeout(greeting.bind(this, caller), 2000);
    },
  };

  return character;
}
Enter fullscreen mode Exit fullscreen mode

Let's create a merchant in main module:

import { createQuestioner } from "./createQuestioner";
import { greeting } from "./greeting";
import { createMerchant } from "./merchant";

async function main() {
  try {
    const questioner = createQuestioner();
    const username = await questioner.ask("Type your username: ");
    const level = await questioner.ask("Type your level: ");

    greeting(username);

    const merchant = createMerchant("Trader", questioner);

    merchant.startDialog({ name: username, level });
  } catch (e) {
    console.error(e);
  }
}

main();
Enter fullscreen mode Exit fullscreen mode

Now, if you run the program and type your name and level (for example, 10) and then answer "yes" in dialog (type "1") when you see something goes wrong with your level:

Excellent! Now your level is: 10
Enter fullscreen mode Exit fullscreen mode

Typical problem with string and number values in JavaScript. Notice that in createMerchant in method startDialog a parameter level has type string but in function greeting the parameter caller has field level with type number. But we don't have any type checking errors after running tsc. We should tell TypeScript to check parameters of function that called by bind() (call(), apply()). This is what option strictBindCallApply is for.

tsconfig.json

{
  "compilerOptions": {
    // ...
    "strictBindCallApply": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, if you run the program you will see that TypeScript highlights the problem with different types of field level in function createMerchant:

src/merchant.ts:21:38 - error TS2769: No overload matches this call.
...
21       setTimeout(greeting.bind(this, caller), 2000);
                                        ~~~~~~
Found 1 error.
Enter fullscreen mode Exit fullscreen mode

TypeScript Strict options in tsconfig.json: strictFunctionTypes

This option is intended for quite specific cases. If this option was set to true then TypeScript will not allow you to use a function in a case when types of parameters of this function are not the same as parameter's types in specified type.

An example:

type loggerFn = (id: number | string) => void;

const logTransaction: loggerFn = (id: string) => {
  console.log(`[${new Date().toDateString()}] ${id.trim()}`);
};

logTransaction(transactionId);
Enter fullscreen mode Exit fullscreen mode

In this case, if options are enabled, tsc will return an error message after running:

src/main.ts:11:11 - error TS2322: Type '(id: string) => void' is not assignable to type 'loggerFn'.
  Types of parameters 'id' and 'id' are incompatible.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.

11     const logTransaction: loggerFn = (id: string) => {
             ~~~~~~~~~~~~~~~
Found 1 error
Enter fullscreen mode Exit fullscreen mode

Theoretically, in this case you could specify the parameter id as a number and call function logTransaction like that: logTransaction(parseInt(transactionId)). But still, you will have a type-checking error because you cannot use method trim() for a number value.

Anyway, is good to know what specific options are needed if you enabled strict mode in your project.

TypeScript Strict options in tsconfig.json: noImplicitThis

You might know that JavaScript has a quite important nuance with the variable "this". Let's say, you have a method that prints a value of an object's field. If you wrote this method as function declaration then will lose "this" of an object where the method is specified. I would say that it's one of the famous "features" of JavaScript and Internet has tons of materials about this.

Here is an example:

const createCharacter = (name: string, level: number) => {
  return {
    label: `[${level} lvl] ${name}`,
    info(prefix: string) {
      return function () {
        console.log(`${prefix}: ${this.label}`);
      };
    }
  };
};

const ranger = createCharacter("Ranger", 77);
const printRangerInfo = ranger.info("Neutral");

printRangerInfo();
Enter fullscreen mode Exit fullscreen mode

After running npm run dev you will see that it throws an error:

TypeError: Cannot read property 'label' of undefined
Enter fullscreen mode Exit fullscreen mode

Now, let's set option noImplicitThis in configuration file:

{
  "compilerOptions": {
    // ...
    "noImplicitThis": true
  }
}
Enter fullscreen mode Exit fullscreen mode

After that TypeScript will highlight an error in the code:

error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
14             console.log(`${prefix}: ${this.label}`);
                                         ~~~~
Enter fullscreen mode Exit fullscreen mode
13           return function () {
                    ~~~~~~~~
An outer value of 'this' is shadowed by this container.
Found 1 error
Enter fullscreen mode Exit fullscreen mode

By doing so we can fix the problem before running an application. One of a solution, in this case, is using an arrow function:

const createCharacter = (name: string, level: number) => {
  return {
    label: `[${level} lvl] ${name}`,
    info(prefix: string) {
      return () => {
        console.log(`${prefix}: ${this.label}`);
      };
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

When you change the nested function to arrow one TypeScript will stop highlight this line as an error. After running npm run dev you will see that the program works correctly.

TypeScript Strict options in tsconfig.json: strictPropertyInitialization

The next option is directly related to classes in JavaScript and TypeScript. In TypeScript, you can specify the properties of the class and also their types. Here is an example.

Let's say we have a special class for game characters:

export class Character {
  name: string;
  level: string;

  constructor() {}

  greeting(callerName: string) {
    console.log(`[${this.level}] ${this.name}: Hello, ${callerName}!`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, in the main module we create a character's object. The character should greetings the player:

async function main() {
  try {
    const questioner = createQuestioner();
    const name = await questioner.ask("Type your first name: ");

    const traveler = new Character();

    traveler.greeting(name);

    questioner.finishUp();
  } catch (e) {
    console.error(e);
  }
}
Enter fullscreen mode Exit fullscreen mode

If you run this small example, you will see:

Type your first name: Byte 
[undefined] undefined: Hello, Byte!
Enter fullscreen mode Exit fullscreen mode

I guess we didn't give a name to the traveler! Okay, we made a mistake in the code. It's not a big deal. The real problem is that TypeScript didn't say anything about it! Notice that constructor of class Character is empty. But also there is no highlighted error or warning. We don't have a specific syntax like required name: string in TypeScript to declare that properties name and level are required for initialization in the class Character. However, we can enable option strictPropertyInitialization and after that TypeScript compiler will tell us that we didn't initialize properties name and level in the constructor method of class Character.

An option strictPropertyInitialization can be enabled only if option strictNullChecks is enabled too.

{
  "compilerOptions": {
    // ...
    "strictNullChecks": true,
    "strictPropertyInitialization": true
  }
}
Enter fullscreen mode Exit fullscreen mode

And after that we run tsc and see:

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

2   name: string;
    ~~~~
Enter fullscreen mode Exit fullscreen mode
src/Character.ts:3:3 - error TS2564: Property 'level' has no initializer and is not definitely assigned in the constructor.

3   level: string;
    ~~~~~

Found 2 errors.
Enter fullscreen mode Exit fullscreen mode

This is exactly what we need. Now, let's fix the problem:

export class Character {
  // Class Property Inference from Constructors
  // since version 4.0 TypeScript can “take" types of properties from a constructor
  // so we don't need to specify types of properties 'name' and 'level' here
  name;
  level;

  constructor(name: string, level: number) {
    this.name = name;
    this.level = level;
  }

  greeting(callerName: string) {
    console.log(`[${this.level}] ${this.name}: Hello, ${callerName}!`);
  }
}
Enter fullscreen mode Exit fullscreen mode

And don't forget to give a name for the traveler in main module!

TypeScript Strict options in tsconfig.json: alwaysStrict

If you set the option alwaysStrict to true then TypeScript will parse your code in ECMAScript Strict mode and put "use strict" in each source file. If you are not familiar with ECMAScript Strict mode then check out article on MDN about it.

Conclusions

When you have already learned what errors can be prevented by TypeScript's strict options you may exclaim "It can be fixed by a few lines of code. Just add a checking of variable's existing before print it. What's the big deal?" and you'll be right. But, it's just a synthetic example to demonstrate the problem that can be solved by strict options. In reality, it could be one small part of a huge project with hundreds of files and thousands of lines of code. You cannot keep track of everything and you shouldn't. You also can make a typo or forget about doing a check because you can't concentrate after last night's party. It can also happen to your new colleague who hasn't completely figured out the codebase yet.

The point is to delegate solving errors that are related to types of variables to tools like TypeScript.

Do you like the material? Please, subscribe to my email newsletter to stay up to date.

subscribe

Top comments (0)