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
function add(a, b) {
return a + b;
}
const result = add(3, "5");
console.log(result);
function add(a: number, b: number): number {
return a + b;
}
const result = add(3, "5");
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());
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());
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
let x: number;
x = 10;
x = undefined;
let y: string | null;
y = "hello";
y = null;
console.log(y.length);
function greet(name: string) {
return "Hello, " + name;
}
greet("Alice");
greet(null);
function printMessage(message?: string) {
console.log(message.toUpperCase());
}
printMessage();
printMessage("Hello TypeScript");
printMessage(undefined);
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 printAnimal(animal: Animal): void {
console.log(animal.name);
}
function printDog(dog: Dog): void {
console.log(dog.name);
}
const printFunc: (animal: Animal) => void = printDog;
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 getDog(): Dog {
return { name: 'Buddy', breed: 'Labrador' };
}
function getAnimal(): Animal {
return { name: 'Buddy' };
}
const getFunc: () => Animal = getDog;
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
}
}