DEV Community

Cover image for 4 techniques for using TypeScript effectively to improve code quality
James Oyanna
James Oyanna

Posted on

4 techniques for using TypeScript effectively to improve code quality

TypeScript is a statically typed superset of JavaScript that offers many benefits such as type safety, better tooling support, and improved code readability.

TypeScript can make your codebase more maintainable and less prone to errors if used correctly.

In this article, we will discuss 4 advanced TypeScript techniques that can help you write cleaner, more maintainable code.

1. Use Advanced Types:

TypeScript provides many advanced types that can make your code more expressive and concise. Some examples include:

Union Types

Union types allow you to define a variable that can hold values of different types. For example, the following code defines a variable that can hold either a string or a number:

let address: string | number;
address = "Lagos"
address = 19;

Here we define a variable called address which can hold values of either type string or number.

We then assign a string value "Lagos" to the address variable. Since we have defined address as a string or number variable, this is perfectly valid.

Similarly, we assign a number value 19 to the address variable, which is also valid since address is defined to accept either a string or a number.

This is an example of using Union Types in TypeScript to define a variable that can hold multiple types.

Using Union Types can be helpful when you have a variable that can hold values of different types, and you want to enforce type safety by ensuring that only certain types of values are assigned to it.

Intersection Types:

Intersection types allow you to combine multiple types into a single type. For example, the following code defines a type that has properties from both Name and Age interfaces:

`interface Name {
name: string;
}

interface Age {
age: number;
}

type UserInfo = Name & Age;

const info: UserInfo = {
name: 'james',
age: 28,
};`

Here we define two interfaces named Name and Age, and a type named UserInfo.

The Name interface has a single property called name of type string and the Age interface also has a single property called age of type number.

We then define UserInfo type using the & (intersection) operator, which means it combines the properties of both Name and Age interfaces. This creates a new type that has all the properties of both interfaces.

The info constant is of type UserInfo, which means it has to have both the name and age properties. In this case, info is an object that has a name property with the value of 'james' and an age property with the value of 28.

Conditional Types

Conditional types allow you to define types that depend on other types. For example, the following code defines a type that returns a number if T is a number, and a string otherwise

`type NumberOrString = T extends number ? number : string;

const a: NumberOrString = 42; // a is of type number
const b: NumberOrString = 'hello'; // b is of type string`

Here, we define a generic type called NumberOrString. The generic type T is used to represent the input type that will be passed to NumberOrString.

The NumberOrString type is defined using a conditional type that uses the extends keyword to create a type that can either be number or string depending on the type of the generic type parameter T.

In this case, the conditional type is checking whether T extends number. If it does, then the resulting type is number. If it does not, then the resulting type is string.

Type Guards:

Type guards allow us to narrow down the type of a variable based on a condition. For example, the following code checks whether a variable is an array:

`function isArray(value: any): value is Array {
return Array.isArray(value);
}

const arr: any = [1, 2, 3];

if (isArray(arr)) {
// arr is now of type Array
console.log(arr.length);
}`

Here, we define an function isArray that takes an argument value of type any and returns a boolean value indicating whether value is an array or not.

The return type of this function is value is Array, which means that if the function returns true, TypeScript should infer that the input value is an array of any type.

The implementation of the isArray function simply checks whether the input value is an array using the built-in Array.isArray method, and returns the result.

2. Use Interfaces and Types

Interfaces and types are the bread and butter of TypeScript. They allow us to define custom types and enforce type safety.
When defining types, it is important to follow some best practices:

Always use interfaces for public API signatures.
Use types for private implementation details.
Use types to define complex types or type aliases.
Enter fullscreen mode Exit fullscreen mode

Here's an example of using interfaces and types:

`interface IUser {
id: number;
name: string;
email: string;
}

type UserList = Array;

function getUsers(): Promise {
// fetch users from the server
}`

Here, we define an interface called IUser with three properties: id which is a number, name which is a string, and email which is also a string.
The interface IUser defines the structure of an object representing a user.

Next, we created a type alias called UserList which is an array of IUser objects. This type is used to define the expected return type of the getUsers function.

Finally, the getUsers function is defined as an asynchronous function that returns a Promise of type UserList.

3. Use Enums:

Enums allow you to define a set of named constants. They can be used to make your code more expressive and self-documenting. Here's an example:

enum LogLevel {
  Debug,
  Info,
  Error,
}

function log(level: LogLevel, message: string) {
  console.log(`[${LogLevel[level]}] ${message}`);
}

log(LogLevel.Info, 'Hello, world!');
Enter fullscreen mode Exit fullscreen mode

Here, we define an enum called LogLevel. enum help us to define a set of named constants.

In this case, LogLevel has three constants: Debug, Info, and Error. By default, the values of these constants start at 0 and increment by 1, but we can explicitly set the values if needed.

In this case, Debug has a value of 0, Info has a value of 1, and Error has a value of 2.

We then define a function called log that takes two parameters: level, which is of type LogLevel, and message, which is a string.
The function logs the message to the console with a timestamp and the level of the log message.

The log function is called with LogLevel.Info as the level and the string 'Hello, world!' as the message. This will result in the following output in the console:

[Info] Hello, world!

4. Use Decorators

Decorators are a feature of TypeScript that allows us to modify classes and class members at runtime. They can be used to add functionality to our classes, such as logging or caching. Here's an example:

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with args
Enter fullscreen mode Exit fullscreen mode

Here, we define a function called log that takes three parameters: target, propertyKey, and descriptor. These parameters are part of the JavaScript/TypeScript language feature called decorators, which can be used to modify or enhance the behavior of a class or its members.

When this log function is invoked, it first creates a variable called originalMethod that holds the original value of the function that is being decorated.

In other words, the log function is intercepting the call to the original function, so it can modify its behavior or log additional information.

Next, the descriptor.value is replaced with a new function that logs a message before invoking the original method. This new function takes an arbitrary number of arguments using the spread syntax (...args: any[]), and logs a message to the console using the console.log method.

In conclusion, this article explored various advanced techniques for using TypeScript effectively to improve code readability and maintainability.

We covered 4 techniques, including the use of advanced types such as Union Types, Intersection Types, Conditional Types, and Type Guards, as well as the use of Interfaces, Types, Enums, and Decorators.

By leveraging these advanced TypeScript features, you can write cleaner and more expressive code, enforce type safety, and make your codebase more maintainable and less prone to errors.

Additionally, by using these techniques consistently throughout your codebase, you can improve code consistency and make it easier for developers to understand and maintain your code.

Top comments (0)