Improving code quality in Typescript with compiler options - Part 2
Tags: javascript, typescript
Published: 2022-07-27 11:12:41

by [Isaac Okoro](https://blog.openreplay.com/authors/isaac-okoro)

Switching from JavaScript to Typescript is a huge improvement in any codebase. The switch can be made even better and improved by going beyond the default typescript features, making our codebase far more secure. To accomplish this, we will need to include some Typescript compiler options. Compiler options are properties in the `tsconfig.json` file that can be enabled and disabled to improve our Typescript experience. It is important to note that as Typescript evolves, new flags will be created; therefore, it is recommended that you review the [typescript documentation](https://www.typescriptlang.org/tsconfig) and see if any new flags may be of interest to you at the time of reading this.

This article is part 2 of [Improving Typescript code quality with compiler options](https://blog.openreplay.com/improving-code-quality-in-typescript-with-compiler-options). Here we will cover more options in the `tsconfig.json` file and how to improve code quality with the `strictPropertyInitialization`, `useUnknownInCatchVariables`, `strictFunctionTypes`, `allowUnusedLabels`, `allowUnreachableCode`, `noPropertyAccessFromIndexSignature`, `exactOptionalPropertyType`, and `forceConsistentCasingInFileNames` options.


## The `tsconfig.json` file 

All compiler options are saved in the `tsconfig.json` file, generated by running `tsc --init` in our project root terminal. In this post, we will add more commonly used compiler options to the `tsconfig.json` file and demonstrate the new functionality they introduce to our code. We will add the following compiler options to improve code quality:

```javascript
"compilerOptions": {
    "module": "system",
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": true,
    "strictFunctionTypes": true,
    "allowUnusedLabels": false,
    "allowUnreachableCode": false,
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyType": true,
    "forceConsistentCasingInFileNames": true,
    "target": "es5", 
    "module": "commonjs", 
    "rootDir": ".",
    "outDir": "../dist/",
    "sourceMap": true 
}
```

## `strictPropertyInitialization`

When set to true, this compiler option ensures we initialize all class properties inside a constructor.

```javascript
class User{
    name: string;
    age: number;
    occupation: string | undefined;
    
    constructor(name: string) {
        this.name = name;
    }
}
```

We have a `User` class in the code block above, and the constructor() method is where we initialize the properties of its instance. JavaScript automatically calls the constructor() method when we instantiate a class object. Typescript requires us to either initialize the properties we defined or specify a type of `undefined`. Hence when we compile the above code, we get the error shown below:

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

Undefined variables in our code cannot be ignored with this option.

## `useUnknownInCatchVariables`

In Javascript, we can throw an error and catch it in a `catch` clause. Often, this will be an instance of error and is set to `any` by default. When the `useUnknownInCatchVariable` compiler option is set to true, it implicitly sets any variable in a `catch` clause to `unknown` instead of `any`. Consider the example below:

```javascript
try {
    // random code...
      throw 'myException'; 
}
catch (err) {
    console.error(err.message); 
}
```

When we compile the above code, with `useUnknownInCatchVariables` set to true, it changes our error to an `unknown` type. Hence we get the error below from the terminal.

```
error TS2571: Object is of type 'unknown'.
```

This error is produced because the error was set to `unknown` by Typescript. To fix this error, we can:

- Narrow the error from `unknown` to an instance of Error.

```javascript
try {
  // random code...
    throw 'myException'; 
}
catch (err) { // err: unknown
  if (err instanceof Error) {
    console.error(err.message);
  }
}
```

- Use a a [type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)

```javascript
try {
  // random code...
  throw "myException";
} catch (err) {
  // Use the type guard
  // error is now narrowed to definitely be a string
  if (typeof err === "string") {
    console.error(err);
  }
}
```

With the help of our `if` check, Typescript recognizes that `typeof err === "string"` is a special form of code where we explicitly state and describe types. This is referred to as [type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html).

## `strictFunctionTypes`

When `strictFunctionTypes` is true, function parameters are checked more thoroughly. Typescript parameters are bivariant by default, implying that they can be both covariant and contravariant. Variance is a way to gain insight into subtyping relationships. When the parameter is covariance, we can assign a specific type to a broader type (e.g., assign a subtype to a supertype). Contravariance is the inverse; here, we can assign a broader type to a specific type (e.g., assigning a supertype to a subtype). We have covariance and contravariance in bivariance. To learn more about variance, click [here](https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance).

```javascript
//strictFunctionTypes: false

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breeds: Array<string>;
}

let getDogName = (dog: Dog) => dog.name;
let getAnimalName = (animal: Animal) => animal.name;

getDogName = getAnimalName;  // Okay
getAnimalName = getDogName;  // Okay 
```    

The above code runs without error, and parameters are compared bivariantly by default. The method of the supertype `getAnimalName` and subtype `getDogName` can be assigned to each other.
Typescript’s parameters are compared contravariantly if `strictFunctionTypes` is set to `true`

```javascript
//strictFunctionTypes : true

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breeds: Array<string>;
}

let getDogName = (dog: Dog) => dog.name;
let getAnimalName = (animal: Animal) => animal.name;

getDogName = getAnimalName; // Okay
getAnimalName = getDogName; // Error
```

When the code block above is run, we get an error:

```
Type '(dog: Dog) => string' is not assignable to type '(animal: Animal) => string'.
Types of parameters 'dog' and 'animal' are incompatible.
Property 'breeds' is missing in type 'Animal' but required in type 'Dog'.
```

Here, `getAnimalName` is a broader function than `getDogName`. As a result, the supertype cannot be assigned to the subtype in this case. However, a subtype can be assigned to a supertype. Most of the time, function parameters should be contravariant, not bivariant. If we enable this typescript option, Typescript will not treat function parameters as bivariant.

## Open Source Session Replay

_[OpenReplay](https://github.com/openreplay/openreplay) is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data._

![replayer.png](https://static.openreplay.com/images/banner-blog.png)

_Start enjoying your debugging experience - [start using OpenReplay for free](https://github.com/openreplay/openreplay)._

## `allowUnusedLabels`

A [label statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label) in JavaScript is used to prefix a label as an identifier.
Label statements in JavaScript are similar to checkpoints in our code. Because the label keyword is not a reserved keyword in JavaScript, we can represent it with any string that is not a reserved word and is followed by a colon `:`. The syntax for declaring a label is similar to the syntax for declaring an object in JavaScript. Most developers make the error of declaring labels instead of objects. To avoid this, Typescript provides us with the `allowUnusedLabels` option, which, when set to `false`, causes Typescript to throw an error highlighting any unused labels in our code.

```javascript
const isUserAgeValid = (age: number) => {
  if (age > 18) {
    curfew: true; //-- COMPILER ERROR - Unused label
  }
};
```

In the code above, `curfew` is a label. The following error will be raised when the code block is compiled:

```
error TS7028: Unused label
```

## `allowUnreachableCode`

UnReachable code will never be executed, such as a code that follows a return statement. When this compiler option is set to `true`, unreachable codes are ignored. In contrast, TypeScript verifies our code paths when `allowUnreachableCode` is set to `false`, ensuring that all codes can be accessed and used. When set to true, an error is thrown if any unreachable code is detected.

```javascript
const randomNum = (n: number): boolean => {
  if (n > 5) {
    return true;
  } else {
    return false;
  }
    
  return true;
};
```

If a code is proven to be unreachable, Typescript will display this warning:

```
error TS7027: Unreachable code detected.
```

## `noPropertyAccessFromIndexSignature`

When this compiler option is set to `true`, it requires us to access unknown properties using `[]` bracket notation and `.dot` notation to access defined properties. This promotes consistency because property accessed with dot notation always indicates existing property. `(obj.key) syntax` While we use the [] bracket notation if the property does not exist. `obj["key"]`.

```javascript
interface HorseRace {
  // defined properties
  breed: "Shetland" | "Hackney" | "Standardbred";
  speed: "fast" | "slow";

  //index signature for properties yet to be defined
  [key: string]: string;
}

declare const pick: HorseRace;

pick.breed;
pick.speed;
pick.ownerName;
```

In the above example, we defined an interface `HorseRace` and gave it some properties such as breed, speed, and an index signature (for unknown properties). We get this error when we compile the code above.

```javascript
error TS4111: Property 'ownerName' comes from an index signature, so it must be accessed with ['ownerName'].
```

To correct this error, use bracket notation when calling the property `ownerName`.

```javascript
pick.breed;
pick.speed;
pick["ownerName"]
```

## `exactOptionalPropertyType`

By default, Typescript ignores whether a property is set to "undefined" because it wasn't defined or because we set it that way.

```javascript
//exactOptionalPropertyTypes = false

interface Test {
  property?: string;
}

const test1: Test = {};
console.log("property" in test1); //=> false

const test2: Test = { property: undefined };
console.log("property" in test2);
```    

The above code block would execute without error, logging `false` and `true` values. In test1, we're checking to see if `property` was defined; if it wasn't, it logs a `false`. In test2, the value `true` is logged because we defined `property` and set `property` to `undefined`. Next, let’s set the `exactOptionalPropertyTypes` option to `true`.

```javascript
//exactOptionalPropertyTypes = true

interface Test {
  property?: string;
}

const test1: Test = {};
console.log("property" in test1); // false

const test2: Test = { property: undefined };
console.log("property" in test2);  // true
```

When the above code block is compiled, the following error will be logged:

```
error TS2375: Type '{ property: undefined; }' is not assignable to type 'Test' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'property' are incompatible. Type 'undefined' is not assignable to type 'string'.
```

What happened here is that Typescript refused to let us define a `property` of `undefined`. If `property` is not explicitly annotated to a type of `undefined` first.
To solve this, we can annotate the `property` with the type `undefined`.

```javascript
//exactOptionalPropertyTypes = true

interface Test {
  property?: string | undefined;
}

const test1: Test = {};
console.log("property" in test1); //false

const test2: Test = { property: undefined };
console.log("property" in test2);  //true
```

Our code runs smoothly again, logging `false` and `true` values. Typescript becomes aware of these two different ways of having an undefined property when `exactOptionalPropertyTypes` is enabled. It also ensures that if we want to explicitly define a property as `undefined`, it must first be annotated with the type `undefined`.

## `forceConsistentCasingInFileNames`

This is a highly recommended compiler option among teams, and it encourages team members to use best practices and to be consistent. When this option is set to `false`, the case sensitivity rules of the operating system (OS) on which Typescript is running are followed. It could be either case sensitive (the operating system distinguishes between lowercase and uppercase characters in file names) or case insensitive (the operating system does not distinguish between character cases). When `forceConsistentCasingInFileNames` option is set to `true`, Typescript will raise an error If we try to import a file with a name casing that differs from the file's name casing on the disk.

```
// StringValidator.ts

export interface StringValidator {
  isAcceptable(s: string): boolean;
}
```

```
// ZipCodeValidator.ts

import { StringValidator } from "./stringValidator";

export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s);
  }
}
```

Without considering the file's name case, we import the `StringValidator` file in the code above. If `forceConsistentCasingInFileNames` option is set to `true`, an error will be raised:

```
error TS1149: File name 'C:/Users/acer/Desktop/workBase/writing/Typescript/tsc/stringValidator.ts' differs from already included file name 'C:/Users/acer/Desktop/workBase/writing/Typescript/tsc/StringValidator.ts' only in casing.
```

To solve this, use the exact character case for your file's name when importing.

## Conclusion

This article took a long look at some compiler options, the errors they generate, how to solve them and how to improve code quality by using them. Many other compiler options that can help with code quality can be found [here](https://www.typescriptlang.org/tsconfig).
  

[![newsletter](https://blog.openreplay.com/static/e6b18eda7a327511296a2f27692f9c70/e9957/newsletter.png)](https://newsletter.openreplay.com/)