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 implicitany
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
WhennoImplicitThis
is enabled, TypeScript will issue a compilation error whenever it detects that the type ofthis
is implicitly of typeany
. This often occurs whenthis
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 isany
if no type information is provided. This can lead to errors, especially in object-oriented code where the context ofthis
is crucial for correct behavior. Consider the following exampleclass 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 ofthis
is lost because it's invoked as a standalone function rather than as a method of an instance ofMyClass
. As a result,this.data
withinfetchData
would likely result inundefined
, leading to unexpected behavior or runtime errors.By enabling
noImplicitThis
in thetsconfig.json
file TypeScript will raise a compilation error for the usage ofthis
infetchData
, indicating that the type ofthis
is implicitly treated asany
. Developers must then either provide explicit type annotations or ensure thatthis
is correctly bound within the function.To resolve the compilation error in the previous example, developers could either bind
fetchData
to an instance ofMyClass
or use arrow function syntax, which automatically captures the lexicalthis
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 ofundefined
Variables declared with a union type that includes
null
orundefined
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 ofAnimal
, TypeScript raises an error becausestrictFunctionTypes
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 assigninggetDog
to a variable of type() => Animal
because the return type (Dog
) ofgetDog
is assignable to the return type (Animal
) of the target function signature.
By enablingstrictFunctionTypes
, 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 syntaximport x from 'module'
, TypeScript generates code that expects the module to export its default value asmodule.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 usingmodule.exports = value
, TypeScript generates code that exports the default value asexports.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
}
}