What the heck is a tsconfig.json file?

Writing code in TypeScript is beneficial, but as a senior developer, understanding how to configure your project to meet your needs is crucial. One place where you can fine-tune project configuration and inform the TypeScript compiler how to compile the code is through a tsconfig.json file. While a default tsconfig.json file is provided when setting up a project (tsc --init), it may not always suit your requirements. In this article, I'll discuss some of the most commonly used options in a TypeScript configuration file.

target

This property specifies the ECMAScript target version for TypeScript compilation. The value can be any valid ECMAScript version like "ES5", "ES6", "ES2015", "ES2016", etc. It determines the level of compatibility with JavaScript engines and which features and syntax TypeScript will allow. We should choose this based on the packages that we are going to use in our project

strict

This is one of the most important options in a tsconfig.json file. This decides how strict your project configuration is and this makes a lot of difference in the quality of your project code. When set to true, it enables all strict type-checking options: noImplicitAny, noImplicitThis, alwaysStrict, strictNullChecks, strictFunctionTypes, etc. Enabling strict mode helps catch more type-related errors at compile-time and promotes writing safer and more predictable code. Let us discuss about each type-checking option and the error it throws if made true

  • noImplicitAny

    The noImplicitAny compiler option in TypeScript is a strict type-checking option that helps enforce stronger typing discipline by flagging variables and parameters with an implicit any type. Let us discuss with an example

      /**
      add function without noImplicitAny in compiler options
      */
      function add(a, b) {
          return a + b;
      }
    
      const result = add(3, "5"); // No compilation error if noImplicitAny is disabled
      console.log(result);
    
      /**
      add function with noImplicitAny in compiler options
      */
      function add(a: number, b: number): number {
          return a + b;
      }
    
      const result = add(3, "5"); // Compilation error: Argument of type 'string' is not assignable to parameter of type 'number'
      console.log(result);
    

    In the above example, when the function is written without noImplicitAny option disabled, it implicitly considers arguments a and b as type any that leads to not throwing any compilation errors. In the 2nd case, when noImplicitAny is enabled, the compiler asks you to explicitly declare types of arguments which donot allow any other types apart from numbers.

  • noImplicitThis
    When noImplicitThis is enabled, TypeScript will issue a compilation error whenever it detects that the type of this is implicitly of type any. This often occurs when this is used in a function or method without a clear type annotation or without being explicitly bound to a particular context.

    By default, TypeScript assumes that the type of this within a function or method is any if no type information is provided. This can lead to errors, especially in object-oriented code where the context of this is crucial for correct behavior. Consider the following example

  •       class MyClass {
              private data: number = 10;
    
              fetchData() {
                  return this.data;
              }
          }
    
          const myObject = new MyClass();
          const fetchDataFunc = myObject.fetchData;
          console.log(fetchDataFunc()); // undefined or runtime error because 'this' is not bound
    

    In this example, when fetchDataFunc is called, the context of this is lost because it's invoked as a standalone function rather than as a method of an instance of MyClass. As a result, this.data within fetchData would likely result in undefined, leading to unexpected behavior or runtime errors.

    By enabling noImplicitThis in the tsconfig.json file TypeScript will raise a compilation error for the usage of this in fetchData, indicating that the type of this is implicitly treated as any. Developers must then either provide explicit type annotations or ensure that this is correctly bound within the function.

    To resolve the compilation error in the previous example, developers could either bind fetchData to an instance of MyClass or use arrow function syntax, which automatically captures the lexical this

class MyClass {
    private data: number = 10;

    fetchData = () => {
        return this.data;
    }
}

const myObject = new MyClass();
const fetchDataFunc = myObject.fetchData;
console.log(fetchDataFunc()); // 10
  • strictNullChecks

    When strictNullChecks is enabled,

    • Variables declared without an explicit type or initialized to undefined are considered to have a type of undefined

    • Variables declared with a union type that includes null or undefined cannot be accessed without first checking for null or undefined.

    • TypeScript raises compilation errors when null or undefined values are assigned to variables that do not allow them.

      Here is an example

        // Example 1: Variable Initialization
      
        let x: number;
        x = 10; // OK
        x = undefined; // Error: Type 'undefined' is not assignable to type 'number'
      
        // Example 2: Union Types
      
        let y: string | null;
        y = "hello"; // OK
        y = null; // OK
        console.log(y.length); // Error: Object is possibly 'null'
      
        // Example 3: Function Parameters
      
        function greet(name: string) {
            return "Hello, " + name;
        }
      
        greet("Alice"); // OK
        greet(null); // Error: Argument of type 'null' is not assignable to parameter of type 'string'
      
        // Example 4: Optional Parameters
      
        function printMessage(message?: string) {
            console.log(message.toUpperCase()); // Error: Object is possibly 'undefined'
        }
      
        printMessage(); // OK
        printMessage("Hello TypeScript"); // OK
        printMessage(undefined); // OK
      
  • strictFunctionTypes

    When strictFunctionTypes is enabled, TypeScript performs stricter checks on function type compatibility, which includes checking parameter types contravariantly (input types are contravariant) and return types covariantly (output types are covariant). This means:

      interface Animal {
          name: string;
      }
    
      interface Dog extends Animal {
          breed: string;
      }
    
      // Function that takes an Animal parameter
      function printAnimal(animal: Animal): void {
          console.log(animal.name);
      }
    
      // Function that takes a Dog parameter
      function printDog(dog: Dog): void {
          console.log(dog.name);
      }
    
      // Assigning 'printDog' to a variable of type 'Animal => void'
      const printFunc: (animal: Animal) => void = printDog; // Error: Argument of type '(dog: Dog) => void' is not assignable to parameter of type '(animal: Animal) => void'.
    

    In this example, even though Dog is a subtype of Animal, TypeScript raises an error because strictFunctionTypes enforces stricter type checking on function assignments. printDog cannot be assigned to a variable of type (animal: Animal) => void because its parameter type (Dog) is not assignable to the parameter type (Animal) of the target function signature.

      // Function that returns a subtype
      function getDog(): Dog {
          return { name: 'Buddy', breed: 'Labrador' };
      }
    
      // Function that returns a supertype
      function getAnimal(): Animal {
          return { name: 'Buddy' };
      }
    
      // Assigning 'getDog' to a variable of type '() => Animal'
      const getFunc: () => Animal = getDog; // OK
    

    In this example, strictFunctionTypes allows assigning getDog to a variable of type () => Animal because the return type (Dog) of getDog is assignable to the return type (Animal) of the target function signature.
    By enabling strictFunctionTypes, TypeScript ensures stricter type compatibility between function types, which helps catch potential type errors early in the development process, leading to more robust and reliable code. However, it may require adjustments in code where looser function type compatibility was previously tolerated.

While there are many other properties, I am not writing about all of them. The above mentioned are the most commonly used properties.

outDir:

  • Defines the directory where TypeScript compiler outputs compiled JavaScript files.

  • It can be an absolute or relative path.

  • All compiled JavaScript files will be placed in this directory while maintaining the same relative directory structure as the source TypeScript files.

module:

  • Specifies the module system used in the generated JavaScript code.

  • Common values include "CommonJS", "AMD", "UMD", "System", "ES6", and "ESNext".

  • The choice of module system affects how TypeScript generates module import/export statements in the output JavaScript files.

esModuleInterop:

  • Without esModuleInterop, when importing a CommonJS module with a default export using the syntax import x from 'module', TypeScript generates code that expects the module to export its default value as module.exports.default.

  • With esModuleInterop enabled, TypeScript generates code that directly accesses the default export, allowing you to use the default import syntax without errors.

  • Without esModuleInterop, when exporting a value as default in a CommonJS module using module.exports = value, TypeScript generates code that exports the default value as exports.default.

  • With esModuleInterop enabled, TypeScript generates code that exports the default value directly, allowing seamless consumption of default exports by ES modules.

There are many other properties that you can explore on the official typescript documentation but these are the most commonly used options that everybody should understand to become a good typescript developer.

Iam leaving the tsconfig.json file setup in my recent project so that it could be a good starting point.

{
  "compilerOptions": {
    "target":"ESNext",
    "module":"esnext",
    "allowJs":false,
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "ES2021",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "noImplicitThis":true,
    "strictFunctionTypes":true,
    "strictBindCallApply": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "strictPropertyInitialization":true,
    "esModuleInterop": true
  }
}