# Advance TypeScript: Take Your Development to the Next Level

By now, we've learned about most of the types and concepts in TypeScript that are vital when working with medium-to-complex applications.

The goal of this article is to explore the more complex and advanced types and concepts used in TypeScript applications. We'll discuss **intersection types, type guards, discriminated unions, typecasting, index properties, function overloading, optional chaining, and nullish coalescing.**

![cover.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646455813217/b8HdkixpQ.png align="left")

## INTERSECTION TYPE:

### DEFINITION & EXAMPLE:

An intersection allows us to combine multiple **types and interfaces** into one single `type` (**not interface**). To create an intersection type, use the `&` operator as follows:

```typescript
type Name = {
  name: string;
};

type Age = {
  age: number;
};

type Person = Name & Age;
```

In the same way, let's create a new `type` by combining two `interfaces`:

```typescript
interface Name {
  name: string;
}

interface Age {
  age: number;
}

type Person = Name & Age;

let p: Person = {
  name: "John",
  age: 18,
};
```

According to the above code, the `Person` type must contain mandatory properties of both the `Name` and `Age` interfaces.

If you miss any of the interface mandatory properties, you'll get a compilation error.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646768657088/ZBmOj89q4.png align="left")

> An interface **CANNOT** be created by intersecting two or more `types` or `interfaces`.

### DIFFERENCE BETWEEN UNION & INTERSECTION TYPES:

* In Typescript, unions and intersections are defined in the **Advanced Types** section.
    
* The main difference between the two is that an `intersection type` combines multiple types into one. Whereas, a `union type` refers to a value that can be one of the multiple types.
    
* The `&` symbol represents an `intersection`, while the `|` symbol represents a `union`.
    
* Those who have a background in programming may notice that these symbols are usually associated with logical expressions. The intersection (`&`) can be considered as an `AND` whereas the union (`|`) can be considered as an `OR`.
    

To better understand the difference, let's look at an example:

```typescript
interface FlyingAnimal {
  flyingSpeed: number;
}

interface SwimmingAnimal {
  swimmingSpeed: number;
}

function getSmallPet(): FlyingAnimal | SwimmingAnimal {
  return {
    flyingSpeed: 1,
  };
}

function getFlyingFish(): FlyingAnimal & SwimmingAnimal {
  return {
    flyingSpeed: 1,
    swimmingSpeed: 1,
  };
}
```

In this example, we have two interfaces: `FlyingAnimal` and `SwimmingAnimal`. Furthermore, we have two functions `getSmallPet` with a return type of `FlyingAnimal | SwimmingAnimal` (union type) and `getFlyingFish` with a return type of `FlyingAnimal & SwimmingAnimal`.

Since the `getSmallPet` return type is union, you should return **all the mandatory properties of at least one** of the types/interfaces included in the union.

Thus, we returned `{ flyingSpeed: 1 }`, which is a property of the `FlyingAnimal` interface.

On the other hand, in `intersection type`, you need to return **all the mandatory properties of every** type/interface included in the intersection.

Therefore, we have returned `{ flyingSpeed: 1, swimmingSpeed: 1 }`, if we miss any of the mandatory properties in `getFlyingFish`, we will receive the following compilation error:

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646457188945/IOtpyhU02.png align="left")

### WHEN TO USE IT?

Instead of using intersections, we can just combine all the properties of the two types/interfaces that we want to intersect.

However, we may have two interfaces that are used separately as types somewhere in the program. To create a new type composed of the properties of these interfaces, we will `intersect` them rather than rewrite their properties.

let's take an example to understand this:

```typescript
interface I1 {
  a: number;
}

interface I2 {
  b: number;
}

let i1: I1 = {
  a: 1,
};

let i2: I2 = {
  b: 2,
};

let combinedI: I1 & I2 = {
  a: 1,
  b: 2,
};
```

There are two interfaces `I1` and `I2` that are used independently in variables `i1` and `i2`.

Additionally, we have the `combinedI` variable, which has an intersection type of both `I1` and `I2`.

So instead of creating a new type/interface for the `combinedI` with properties `a` and `b`, we just intersect the available interfaces `I1` and `I2`.

## TYPE GUARDS:

* A **Type Guard** reduces the type of an object in a conditional block. Basically, it is an additional piece of code we write to prevent errors at runtime
    
* Some of the most famous **Type Guards** are `typeof`, `in`, and `instanceof`.
    

### `typeof` OPERATOR:

In our previous blog [Everyday Types in TypeScript](https://blog.wajeshubham.in/everyday-types-in-typescript#heading-handle-compilation-error-using-type-guards), we looked at how type guards could be used to add type-checking to avoid compilation errors.

To understand it better, let's look at some more examples:

```typescript
type alphanumeric = string | number;

function add(a: alphanumeric, b: alphanumeric) {
  if (typeof a === "number" && typeof b === "number") {
    return a + b;
  }

  if (typeof a === "string" && typeof b === "string") {
    return a.concat(b);
  }

  throw new Error("Invalid parameters"); // return when a and b are not of the same type
}
```

In this example, we have a union-type `alphanumeric`, which can be either `string` or `number`. Also, we have a simple `add` function that has two parameters `a` and `b` of the type `alphanumeric`.

In it, we add a type guard for checking whether both parameters have the same types, and then we perform some operations.

```typescript
if (typeof a === "number" && typeof b === "number") {
    return a + b;
  }
```

This block checks whether parameters `a` and `b` are `number` types. If they are, we will **add** them.

```typescript
 if (typeof a === "string" && typeof b === "string") {
    return a.concat(b);
  }
```

This block checks whether parameters `a` and `b` are `string` types. If they are, we will **concatenate** them.

### `instanceof` OPERATOR:

As discussed in our previous blog on [Classes in TypeScript](https://blog.wajeshubham.in/classes-in-typescript), the `class` can be used as a valid TypeScript type.

The `instanceof` operator can be used to check whether a variable is an instance of a class.

Here's an example to help you understand:

```typescript
class Car {
  drive() {
    console.log("Driving a car...");
  }
}

class Truck {
  drive() {
    console.log("Driving a truck...");
  }
  loadCargo() {
    console.log("Loading cargo on truck...");
  }
}

type Vehicle = Car | Truck;

function useVehicle(v: Vehicle) {
  v.drive();
  v.loadCargo(); // we get compilation error here
}
```

We have two classes in the above code: `Car` and `Truck`. Both classes have a common method called `drive()`, while class `Truck` has an additional method called `loadCargo()`.

Then we are creating a new type called `Vehicle`, which is a union of `Car` and `Truck`.

Hence, the `Vehicle` type may or may not have all the properties of both `Car` and `Truck`.

Furthermore, we have a function `useVehicle` which accepts a parameter `v` of type `Vehicle`, and inside it, we access the methods `drive()` and `loadCargo()`.

However, we are unable to access the `loadCardo()` method due to a compilation error. Take a look at the following picture:

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646460145853/wMaQwokzc.png align="left")

This is because the `drive()` method is available in both `Car` and `Truck` classes, but the `loadCargo()` method is only available in the `Truck` class. Since the `Vehicle` is a union type, there is a chance that you could pass a parameter of type `Car` that does not have the `loadCargo()` method.

To avoid this error, we can create a type guard by using the `instanceof` keyword:

```typescript
function useVehicle(v: Vehicle) {
  v.drive();
  if (v instanceof Truck) { // type guard
    v.loadCargo(); // runs without error
  }
}
```

This means that only if `v` is an instance of the class `Truck`, execute the next line of code.

### `in` OPERATOR:

The `in` operator performs a safety check on the existence of a property in an `object`. This can also be used as a `type guard`. For instance:

```typescript
interface Developer {
  developmentTools: string[];
}

interface Tester {
  testingTools: string[];
}

type Employee = Developer | Tester;

function getToolsUsed(employee: Employee) {
  console.log(employee.developmentTools); // compilation errors
  console.log(employee.testingTools); // compilation errors
}

let developer: Employee = {
  developmentTools: ["typescript", "react"],
};
```

We have two interfaces, `Developer` and `Tester`, with both having the `developmentTools` and `testingTools` properties of type `string[]` respectively.

After that, we declare a new type `Employee` that is a union type.

We also have a function `getToolsUsed` that accepts a parameter `employee` of type `Employee`.

We thus encounter a compilation error when we try to access a `developmentTools` or a `testingTools` through the `employee` parameter because that object may or may not have those properties.

We can prevent this by adding a `type guard` using the `in` operator, as follows:

```typescript
function getToolsUsed(employee: Developer | Tester) {
  if ("developmentTools" in employee) {
    // only works if the employee has the property developmentTools
    console.log(employee.developmentTools); // runs without error
  }
  if ("testingTools" in employee) {
    // only works if the employee has the property testingTools
    console.log(employee.testingTools); // runs without error
  }
}
```

### DISCRIMINATED UNION:

Discriminated union type guard is a design pattern you use to ensure a runtime error won't occur because the property that doesn't exist is accessed.

As an example, let's replace `in` in the above example with a discriminated union-type guard.

```typescript
interface Developer {
  role: "developer";
  developmentTools: string[];
}

interface Tester {
  role: "tester";
  testingTools: string[];
}

let Employee: Developer | Tester;

function getToolsUsed(employee: Developer | Tester) {
  switch (employee.role) {
    case "developer":
      console.log(employee.developmentTools); // valid code
      break;
    case "tester":
      console.log(employee.testingTools); // valid code
      break;
  }
}
```

To all of the interfaces that are included while creating a `union type`, we are assigning a `role` property - a `literal type` (click [here](https://blog.wajeshubham.in/everyday-types-in-typescript#heading-literal-types) for more information about literal types).

Lastly, we used the `role` property as a differentiator in switch-case statements.

## INDEX PROPERTIES:

Index signatures look similar to property signatures, but with one difference. Instead of writing the property name, you simply place the type of key within square brackets.

### SYNTAX AND DECLARATION:

Take a look at the following code:

```typescript
interface StringObject {
  [key: string]: string;
}

const strings: StringObject = {
  en_US: "Hello, World!",
  fr_FR: "Bonjour, le monde!",
  es_ES: "¡Hola, mundo!",
  de_DE: "Hallo, Welt!",
};
```

Here we have a `StringObject` interface with a `string` `key` and `string` `value` structure. We will receive the following compilation error if we break the rules:

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1645469724439/4rpWcqd4z.png align="left")

### USE CASE OF INDEX SIGNATURE:

The purpose of index signatures is to type objects of unknown structure when only key and value types are known.

To understand this, let's look at an example.

```typescript
interface SalaryStructure {
  [key: string]: number;
}

let developerSalary: SalaryStructure = {
  baseSalary: 10000,
  bonus: 100,
  incentive: 50,
  appreciationBonus: 10,
};

const getTotalSalary = (salaryObject: SalaryStructure): number => {
  let total = 0;
  for (const name in salaryObject) {
    total += salaryObject[name];
  }
  return total;
};

console.log(getTotalSalary(developerSalary)); // prints: 10160 rupees
```

Above we have an interface `SalaryStructure` that is declared with an **index signature**. This demonstrates that the `SalaryStructure` interface will have `keys` of type `string` and values of type `number`.

Next, we declare a variable `developerSalary` which is of type `SalaryStructure`. Furthermore, we are creating a function `getTotalSalary` that accepts a `salary` object of type `SalaryStructure`.

As we know that `salaryObject` will be an object with a `string` key and a `number` value, we can use the for loop to loop through the `salaryObject` to calculate the total salary.

### DISADVANTAGE:

This syntax has the drawback of not having auto-suggestions supported by the IDE. Because IDE doesn't know the exact key that an object holds, it only knows the type of key it holds.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1645469844387/yYo462Q7B.png align="left")

In addition, it doesn't raise a compilation error when we try to access a key that doesn't exist in the object, as you can see in the following image.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1645470018973/nt33xVmu7.png align="left")

## TYPECASTING:

* In typecasting, a variable is transformed from one type to another.
    
* JavaScript does not have a concept of type casting since variables have dynamic types.
    
* By typecasting, you can tell TypeScript that a particular value is of a specific type, which would otherwise be impossible for TypeScript to detect by itself.
    

### ACCESSING A DOM ELEMENT:

TypeScript warns you when properties/methods are called on DOM elements.

Here's an example to help you understand:

> To run the following code snippets, you need to have `typescript` installed globally. If you don't have `typescript` installed, run `npm install -g typescript` in your terminal or check out my blog on [Introduction to TypeScript](https://blog.wajeshubham.in/introduction-to-typescript#heading-lets-set-up-a-project) to get started.

In the root folder, create the `index.html` and `index.ts` files.

In `index.html`, add the following code:

```xml
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Typescript</title>
  </head>
  <body>
    <input type="text" id="my-inp" />
    <script src="./index.js"></script>
  </body>
</html>
```

In `index.ts`, add the following code:

```typescript
let myInput = document.getElementById("my-inp");

console.log(myInput.value) // throws error
```

In the above code, we are trying to get a DOM element by its `id`. The problem is that TypeScript does not know what type of element we are attempting to access.

So it infers it to be an element with type `HTMLElement | null`, as seen in the following image:

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646469084811/hxoQUvgoF.png align="left")

As a developer, we know that the `myInput` can't be `null` since `index.html` has an `input` tag with the `id="my-inp"`.

To inform TypeScript that the element with the `id="my-inp"` exists we can use `!` at the end of the value that might be `null`, as follows:

```typescript
const myInput = document.getElementById("my-inp")!;
```

Now, if you hover over `myInput` you will see the type as `HTMLElement`.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646469305664/5K65lB5Ag.png align="left")

However, all DOM elements that are `HTMLElement` does not have a property called `value`. Yet, as a developer, we are aware that we are trying to access an `input` element that has a `value` property.

Therefore, to tell TypeScript that this element is of type `HTMLInputElement`, we shall use **Type Casting**.

For type casing, there are two different syntaxes to choose from:

### TYPECAST USING `<>` OPERATOR:

The syntax we use to typecast the above code snippet uses the `<>` operator is as follows:

```typescript
const myInput = <HTMLInputElement>document.getElementById("my-inp")! ;

console.log(myInput.value) // runs without error
```

> It is not a recommended way of doing typecasting because it is similar to **JSX**.

### TYPECAST USING `as` KEYWORD:

The syntax we use to typecast the above code snippet uses the `as` keyword is as follows:

```typescript
const myInput = document.getElementById("my-inp")! as HTMLInputElement;

console.log(myInput.value); // runs without error
```

## FUNCTION OVERLOADING:

TypeScript supports the concept of `function overloading`. Multiple functions can share the same name, but have different parameter types and return types. However, the number of parameters must be the same.

Let's take an example to understand this concept.

```typescript
type Param = string | number;

function add(a: Param, b: Param) {
  if (typeof a === "string" || typeof b === "string") {
    // even if any of the value in `number` we are converting to string
    return a.toString() + b.toString();
  }
  return a + b;
}

let result = add("John ", "Doe");
```

In the code above, we have a custom type called `Param` that is a union type. Furthermore, we have an `add` function that accepts two parameters `a` and `b`, both of type `Param`.

Let's see what inferred type the `result` variable has if we run the `add` function with both parameters of the type `string`.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646474189243/IHsxMQcC0.png align="left")

As you can see in the above image, it appears to be a `string | number` variable, which is correct to a certain extent, but it would cause a problem if we attempted to access any string method on the `result` variable.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646474354756/Py3RGVY_q.png align="left")

Since we are passing two `strings`, the return type must also be a `string`.

To fix this, we are going to implement function overloading as follows:

```typescript
type Param = string | number;

function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: number, b: number): number;
function add(a: Param, b: Param) {
  if (typeof a === "string" || typeof b === "string") {
    // even if any of the value in `number` we are converting to string
    return a.toString() + b.toString();
  }
  return a + b;
}

let result = add("John ", "Doe");
result.split(" "); // ["John", "Doe"] runs without an error
```

Here we are overloading a function `add` by specifying all possible combinations of parameter types and expected return types. Therefore, now if we attempt to determine the inferred type of the `result` variable, we will get a `string`.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646474524837/V7nCqiaIb.png align="left")

## OPTIONAL CHAINING:

Consider a situation where you are making an API call and you don't know what data you will receive in the response.

In that case, you must use additional logic before accessing the nested object within the response data.

Let's take an example to understand this:

```typescript
let getCourse = async () => {
  // following api call will return {"id":1,"name":"typescript","description":"this is the description"}
  await fetch(
    "https://jsonware.com/api/v1/json/7ece5b94-268b-4b86-a3cd-aa0eb2f9b62b",
    {
      method: "GET",
    }
  )
    .then((res) => res.json())
    .then((data) => {
      console.log(data); // data is the response from the API which is uncertain
    });
};

getCourse();
```

Our async function `getCourse` calls an API, and once the request is successful, we return the resultant object as `data`.

For instance, let's say a `data` object has an `id`, `name`, and `description` keys, but we accidentally accessed the `price` key, which doesn't exist.

Since the `data` has been inferred to be of `any` type, TypeScript will not throw an error instead it will ignore it.

Let's check out what happens when we access the `price` key.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646485218474/JAI32JRs5.png align="left")

Here is what we see in the console.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646485263049/1817eP_0f.png align="left")

It will throw a runtime error that states `Cannot read properties of undefined (reading 'toFixed')`.

In this case, we can use **Optional Chaining** by adding `?` after the key name that might be `null` or `undefined` as follows:

```typescript
// ...
 .then((data) => {
      console.log(data.price?.toFixed(2)); // this will fail silently
 });
// ...
```

Here is what we see in the console.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646485403079/vpJcu90Ms.png align="left")

It prints `undefined` without throwing a runtime error.

## NULLISH COALESCING:

Nullish coalescing is a loosely related concept to **optional chaining**, which is used to deal with `undefined` or `null` values.

Let's take an example to help you understand:

```typescript
let inputValue = null;

let ignoreNullAndUndefined = inputValue || 'default';

console.log(ignoreNullAndUndefined); // prints 'default'
```

We have a variable called `inputValue` with the value `null`, as well as a variable called `ignoreNullAndUndefined` that accepts values other than `null` and `undefined`.

Therefore, we used the `||` (OR) operator to ensure `ignoreNullAndUndefined` does not receive `null` or `undefined` values.

The problem occurs when we declare `inputValue` as `""` (empty string). Because the `||` (OR) operator treats it as a `falsey` value, which we don't want.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646486182146/gzqETsFWf.png align="left")

Only `null` and `undefined` should be discarded.

Therefore, we can use the **nullish coalescing operator** `??` to only avoid `null` and `undefined` as follows:

```typescript
let inputValue1 = 0;
let inputValue2 = "";
let inputValue3 = false;
let inputValue4 = null;

let ignoreNullAndUndefined1 = inputValue1 ?? "default";
let ignoreNullAndUndefined2 = inputValue2 ?? "default";
let ignoreNullAndUndefined3 = inputValue3 ?? "default";
let ignoreNullAndUndefined4 = inputValue4 ?? "default";

console.log(ignoreNullAndUndefined1); // prints 0
console.log(ignoreNullAndUndefined2); // prints ""
console.log(ignoreNullAndUndefined3); // prints false
console.log(ignoreNullAndUndefined4); // prints "default"
```

`??` will only and only discard `null` and `undefined` and accept any other value, regardless of its truthiness or falsity.

## CONCLUSION:

* Despite TypeScript being very simple to grasp when performing basic tasks, knowing how its type system works is crucial to unlocking its advanced capabilities.
    
* Once we understand how TypeScript works, we can use this knowledge to write cleaner, well-organized code.
    
* Hopefully, this article has helped to demystify parts of the TypeScript type system and give you some ideas about how you can exploit its advanced features to improve your TypeScript application structure.
    

Make sure to subscribe to our newsletter on [https://blog.wajeshubham.in/](https://blog.wajeshubham.in/) and never miss any upcoming articles related to TypeScript and programming just like this one.

I hope this post will help you in your journey. Keep learning!

My [***Website***](https://wajeshubham.in), connect with me on [LinkedIn](https://www.linkedin.com/in/shubham-waje/) and [GitHub](https://github.com/wajeshubham)
