5 Tips To Improve Your TypeScript Code
Published: 2023-07-18 19:16:11

by [Olasunkanmi Balogun](https://blog.openreplay.com/authors/olasunkanmi-balogun)

<blockquote><em>TypeScript is rapidly gaining popularity as a way to write type-safer, maintainable JavaScript code. However, while it is relatively easy to start, mastering it requires knowledge of its advanced features; learning TypeScript isn't always easy. This article will cover five tips that will help you improve your TypeScript code, from using set theory to mastering enums and generics.</em></blockquote>

The tips in this article will help you utilize TypeScript's type-checking to its full potential, producing improved code quality for your server and web development tasks. To get the most out of this text and become a great software developer, you must have sufficient knowledge of JavaScript, including variables, data types, and methods. To follow along with code samples provided in this article, you can make use of the official [TypeScript playground.](https://www.typescriptlang.org/play)

When you are ready, let's dive in.

## 1. Types As Sets

In sets theory, a set is a collection of elements distinct from each other and share some common properties.

The knowledge of set theory will help you better infer the behavior of types in TypeScript. A type is a set of possible values that a variable can hold. For example, the type `number` is a set of all possible numbers.

```typescript
let x: number; // a set of all possible numbers 
x = 123 // 123 is a possible number
x = 'Hello' // will throw an error because hello is a string, not a number
```

From the above code, the variable `x` is a member of the type `number` because it can hold a number, in this case, `123`. However, attempting to assign a `string`, such as `"Hello"`, triggers an error because `x` is not a member of the `string` type. Hence, we can say `x` is a proper subset of the type `number`.

Further, for an assignment to succeed if two types are not the same, a **type-casting** process occurs. **Type-casting** is a process of overriding a type. This process occurs in two forms: **upcasting** and **downcasting**.

During the process of **upcasting**, a variable of a more specific type is assigned to a variable of a more general type. To better explain this consider the two variables with different types in the code below:

```typescript
let name: string = 'John';
let nameLiteral: 'Patrick' | 'James' | 'Wood' = 'Patrick';

name = nameLiteral; // possible because of upcasting
```

The variable `name` has a type of `string`, and the variable `nameLiteral` has a type of three specific strings: `Patrick`,`James`, and `Wood`. The assignment of `nameLiteral` to `name` is allowed because the type of `nameLiteral` is a proper subset of the type of `name`, which makes it type-safe. In other words, `Patrick`, `James`, or `Wood` are all strings, and since `name` is of type `string`, the TypeScript compiler doesn't see a problem.

However, reversing the assignment will be **downcasting**, which is normally disallowed. This is because `name` is a member of a larger set - `string` than `nameLiteral` - `Patrick` | `James` | `Wood`.

```typescript
let name: string = 'John';
let nameLiteral: 'Patrick' | 'James' | 'Wood' = 'Patrick';

nameLiteral = name; // impossible because of downcasting
```

The concept of set operators, like `union`, `intersection`, `difference`, and `complements`, can also be used to combine existing sets into new sets to understand type combinations in TypeScript.

However, TypeScript only uses two of these operators as type operators: the `|` operator for union and the `&` operator for intersection. However, they behave counter-intuitively when utilizing them to combine types (interfaces).

Consider the code snippet below:

```typescript
type User = {
   getId(): number;   
   getName(): string;
}

type Admin = {
   getId(): number;
   getUserName(): string;
}

declare function Person(): User | Admin;

const person = Person()

person.getId() // succeeds
person.getName() //fails
person.getUsername() // fails
```

The `|` operator in TypeScript, known as a union type operator, can sometimes be misleading for any developer accustomed to thinking of the `|` operator as a logical `OR` operator in other contexts, such as boolean logic. Instead, it creates a type that includes the intersection of properties and methods shared by the constituent types.

Hence, the union of `User` and `Admin` will return only the properties and methods that are common to both.

On the other hand, thinking of the `&` operator in terms of logical `AND` `(&&)` can also be misleading. In the code snippet below, you see how intersecting two types (interfaces) returns an output of all methods in both types.

```typescript
type User = {
   getId(): number;   
   getName(): string;
}

type Admin = {
   getId(): number;
   getUserName(): string;
}

declare function Person(): User & Admin;

const person = Person()

person.getId() // succeeds
person.getName() //succeeds
person.getUsername() //succeeds
```

In summary, the `|` operator in TypeScript represents a union type and takes the intersection of the types involved. In contrast, the `&` operator represents an intersection type and combines the features of multiple types. Understanding this distinction is important to avoid misconceptions and accurately model the intended behavior in TypeScript code.


## 2. Utilize Discriminated Unions As An Alternative To Optional Fields

Imagine you are developing a TypeScript application that interacts with various APIs, and you need to handle different types of responses, such as successful data retrieval or error notifications. For this, you might typically use optional fields or conditional type checks, like in the following example:

```typescript
interface APIResponse {
 success: true | false;
 data?: number;
 error?: string;
}

function handleAPIResponse(response: APIResponse) {
 return (response.success) ? response.data : response.error
}
```
Although optional fields may appear convenient, it's important to note that the aforementioned function could potentially return `undefined`. This is because both the `data` and `error` values can either be `undefined` or the specific values defined within the interface property. To visualize this concept, please refer to the accompanying image for a clearer understanding.

![-](https://blog.openreplay.com/images/five-tips-to-improve-your-typescript-code/images/image01.png)

Instead, with discriminated unions we can create a relationship between the two interfaces which provides a more elegant and type-safe solution:

```typescript
interface SuccessResponse {
 success: true;
 data: number;
}

interface ErrorResponse {
 success: false;
 error: string;
}

interface APIResponse = SuccessResponse | ErrorResponse;

function handleAPIResponse(response: APIResponse) {
 (response.success) ? response.data: response.error
}
```
With a relationship established with discriminated unions, the function will return specific values defined in the interface property, as seen in the images below:

![-](https://blog.openreplay.com/images/five-tips-to-improve-your-typescript-code/images/image02.png)

Instead of using optional fields, we can assure type safety, manage variant-specific logic, and improve code readability and maintainability by using discriminated unions.




<div style="background-color:#efefef; border-radius:8px; padding:10px; display:block;">
<hr/>
<h3><em>Session Replay for Developers</em></h3> 
<p><em>Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data.</em></p>
<img alt="OpenReplay" style="margin-top:5px; margin-bottom:5px;" width="768" height="400" src="https://raw.githubusercontent.com/openreplay/openreplay/main/static/openreplay-git-hero.svg" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<p><em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em><p>
<hr/>
</div>



## 3. Use Type Predicates To Narrow Down Types

To narrow down the type of a function's return value in TypeScript, predicates come in handy. With type predicates, you can define a function that returns a boolean type to check if a value is of a particular type.

Predicates are often defined in the form of `variable is type` and are used on the return type of functions, like this:

```typescript
function func(var: any): var is Type {
 return /* boolean expression */
}
```

In the sample function above, `func` accepts a parameter `var` of type `any` and returns a boolean value. The `var is Type` expression indicates that its return value is a type predicate.

Now that you understand its syntax, let's explore how type predicates can be used in practice:

```typescript
type Fish = {
 swim:() => void
}

type Bird = {
 fly:() => void
}

function isFish( pet: Fish | Bird ): pet is Fish {
 return (pet as Fish).swim !== undefined 
}
```

In the above example, we defined two [types](#https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-aliases): `Fish` that has a method `swim` and `Bird` that has a method `fly`.

By indicating the type predicate `pet is Fish` in the function `isFish`, we signify to the compiler that if the expression `(pet as Fish).swim !== undefined` is true, that is, if `pet` has the method `swim`, then the value will belong to the `Fish` type; otherwise it will be classified as the `Bird` type. This concept is exceptionally handy because it permits the compiler to narrow down the type, guaranteeing that we are solely handling the specific type within the control statement.

By using this construct, we can convey the accurate types to the compiler, enabling it to understand the types we are working with, as seen below:

```typescript
function getFood(pet: Fish | Bird) {
   if(isFish(pet)) {
       pet // if true, pet is of type Fish
       return 'fish food' 
   }
   else {
       pet //if false, pet is of type Bird
       return 'bird food'
   }
}
```

By using type predicates, you can provide the compiler with additional information about a variable type that will be used subsequently; this helps the compiler to make precise [type inferences](https://www.typescriptlang.org/docs/handbook/type-inference.html#handbook-content). This can also help us as developers to make debugging processes faster.

## 4. Improve Code Organization With Enums

In TypeScript, Enums are data types that enable you to write a set of named constants. Enums define related values as `numbers` or `strings`. Since they allow grouping related values, it is a very useful way to organize our code, which aids in type safety.

To define an `enum`, use the `enum` keyword followed by its name, then its named constant in curly braces, like this:

```typescript
enum Color {
  Red,
  Green,
  Blue
}
// usage
let color = Color.Red; 
```
By default, the first `enum` key defaults to `0` unless defined otherwise(it can also be defined as a string or assigned a custom numeric value), then increments by `1` for each member. This is what the above example looks like under the hood:

```typescript
enum Color {
  Red = 0,
  Green = 1,
  Blue = 2,
}
```

You can also decide the numeric value of the first member of the enumeration, then other members increment by `1` from that value:

```typescript
enum Color {
   Red = 1,
   Green,
   Blue
}
```
The above illustration translates to `Red = 1`, `Green = 2`, and `Blue = 3`.

`Enums` are particularly useful in cases where you have a finite number of constant values, like the days of the week. Using an `enum` can provide a more structured and type-safe approach to working with those values:

```typescript
enum Weekdays {
 Sunday = 1,
 Monday
 Tuesday,
 Wednesday,
 Thursday,
 Friday,
 Saturday
}
```

To learn more about enums, check out this [article.](#https://blog.logrocket.com/writing-readable-code-with-typescript-enums-a84864f340e9/)

## 5. Improve Code Flexibility With Generics

TypeScript's generics enable the development of reusable, type-safe components that can handle a wide range of data types. By enabling the development of functions, [classes](#https://www.typescriptlang.org/docs/handbook/2/classes.html), and [interfaces](#https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#interfaces) that can operate on any data type as long as it complies with specific requirements, they increase the flexibility of programming.

Generics are declared in TypeScript code as `<T>`, where `T` stands for a type that has been passed in - you can interpret `<T>` as a generic of type `T` as placeholder for a type that will be defined when an instance of the structure is generated, `T` will act in this situation similarly to how parameters do in functions.

To illustrate a practical example of the above explanation, we'll declare a function with a generic type `T` - type `T` also being the type of the argument and return value as seen below:

```typescript
function func<T>(value: T):T {
  return value;
}

const result = func('Hello John');
```

`T` now has the type `Hello John`, the exact argument passed to the function call:

![-](https://blog.openreplay.com/images/five-tips-to-improve-your-typescript-code/images/image05.png)

This also applies to the variable `result`:

![-](https://blog.openreplay.com/images/five-tips-to-improve-your-typescript-code/images/image06.png)

However, you may want your code to return a desired type. You can be specific by setting the desired type as the generic type parameter, like this:

```typescript
function func<T>(value: T): T {
 return value;
}
const result = func<number>('Hello John');
```
In the above example, type `T` is now of type `string`:

![-](https://blog.openreplay.com/images/five-tips-to-improve-your-typescript-code/images/image07.png)

Generics allow you to build reusable, adaptable components that can handle a variety of data types without compromising type safety or code readability. As a result, you can quickly adapt and expand your components to operate with new data types as necessary, giving your code greater flexibility and maintainability over time. To gain more knowledge on generics, this [article](#https://www.digitalocean.com/community/tutorials/how-to-use-generics-in-typescript) is a must-read.

## Conclusion

In conclusion, TypeScript is a powerful tool for writing scalable and maintainable code. By incorporating type annotations, union types, type predicates, enums, and generics, you can greatly improve your code's type safety and flexibility. Type annotations help catch errors early and provide better documentation, while union types and enums make code more readable and self-explanatory. Type predicates and generics add more flexibility to code, allowing you to create reusable and adaptable functions and [classes](https://www.typescriptlang.org/docs/handbook/2/classes.html#handbook-content).

These pointers, however, only scratch the surface of TypeScript's features and advantages. You are urged to learn more about the language and find new ways to improve your code. Because of TypeScript's robust tooling, tight typing, and support for contemporary JavaScript code features, you can produce better code more quickly.