Altcademy - a Forbes magazine logo Best Coding Bootcamp 2023

What is Narrowing in TypeScript?

In this blog post, we will explore a concept in TypeScript called "narrowing." As a programmer learning TypeScript, you will often come across this term, and it's essential to understand what it means and how it works. We will try to provide you with a clear understanding of narrowing in TypeScript by explaining it in simple terms without using too many jargons. And when we do use a jargon, we will make sure to provide an explanation.

Before we dive into narrowing, let's first discuss what TypeScript is, and why it's important.

What is TypeScript?

TypeScript is a programming language that is a superset of JavaScript. What this means is that TypeScript extends JavaScript by adding types to the language. TypeScript enables you to write safer and more reliable code by catching errors before your code is executed.

In JavaScript, you don't have to declare the types of your variables, which can sometimes lead to unexpected bugs. TypeScript, on the other hand, allows you to declare the types of your variables, making it easier to catch errors early in the development process.

Now that we have a basic understanding of TypeScript, let's dive into the concept of narrowing.

What is Narrowing?

Narrowing, in simple terms, refers to the process of refining the type of a variable based on the information available in the code. This helps TypeScript to provide more accurate type-checking and improve code safety. With narrowing, TypeScript can make sure that you are using the correct types for your variables, properties, and functions.

To give you an analogy, imagine that you have a box of different fruits, and you want to pick out all the apples. You would start by looking at each fruit and determining whether it's an apple or not. If it is an apple, you put it in a separate box for apples. In this example, you are narrowing down the type of fruit in the box to only include apples.

In TypeScript, we perform narrowing to ensure that our code is safe and that we are using the correct types for our variables and functions.

Let's look at some code examples to understand narrowing better.

Basic Example of Narrowing

Consider the following code snippet:

let myVar: string | number;

myVar = "Hello, World!";

if (typeof myVar === "string") {
  console.log(myVar.toUpperCase()); // This is safe because TypeScript knows myVar is a string
}

In this example, we declare a variable myVar with the type string | number. This means that myVar can be either a string or a number. Later in the code, we assign the value "Hello, World!" to myVar, which is a string.

Inside the if statement, we check if the type of myVar is a string using the typeof operator. If it is indeed a string, we call the toUpperCase() method on myVar.

At this point, TypeScript knows that myVar is a string because we explicitly checked its type inside the if statement. This is an example of narrowing. TypeScript has narrowed the type of myVar from string | number to just string inside the if statement, ensuring that our code is safe.

Narrowing with User-Defined Type Guards

TypeScript provides a feature called "user-defined type guards" that allows you to create custom type-checking functions. These functions can be used to narrow the types of your variables even more accurately.

Let's look at an example:

interface Circle {
  type: "circle";
  radius: number;
}

interface Square {
  type: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function isCircle(shape: Shape): shape is Circle {
  return shape.type === "circle";
}

const myShape: Shape = { type: "circle", radius: 5 };

if (isCircle(myShape)) {
  console.log("The area of the circle is:", 3.14 * myShape.radius * myShape.radius);
}

In this example, we have two interfaces, Circle and Square, and a type alias Shape that can be either a Circle or a Square. We then define a function called isCircle, which takes a Shape as an argument and returns a boolean.

Notice the return type of the isCircle function: shape is Circle. This is a special syntax in TypeScript that tells the compiler that this function is a type guard. When the function returns true, TypeScript knows that the argument is of the type Circle.

In our example, we have a constant myShape of type Shape, which is assigned a Circle object. We then use the isCircle function to check if myShape is a Circle. If it is, we calculate the area of the circle using the radius property. Thanks to the type guard we defined, TypeScript narrows the type of myShape to Circle inside the if statement, ensuring that our code is safe.

Discriminated Unions and Narrowing

Discriminated unions are a powerful feature in TypeScript that allows you to create complex types with a shared property, which can be used to narrow the type of a variable.

Let's revisit the previous example with the Circle and Square interfaces:

interface Circle {
  type: "circle";
  radius: number;
}

interface Square {
  type: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function calculateArea(shape: Shape): number {
  if (shape.type === "circle") {
    return 3.14 * shape.radius * shape.radius;
  } else if (shape.type === "square") {
    return shape.sideLength * shape.sideLength;
  }

  throw new Error("Unknown shape type");
}

const myShape: Shape = { type: "circle", radius: 5 };
console.log("The area of the shape is:", calculateArea(myShape));

In this example, we have the same Circle and Square interfaces and the Shape type alias. Instead of using a type guard function, we define a function called calculateArea that takes a Shape as an argument and returns a number.

Inside the function, we use the type property (which is our discriminant) to check if the shape is a Circle or a Square. If it's a Circle, we calculate the area using the radius property, and if it's a Square, we calculate the area using the sideLength property. TypeScript narrows the type of shape inside each branch of the if statement, ensuring that our code is safe.

Conclusion

Narrowing is an essential concept in TypeScript that allows you to refine the types of your variables based on the information available in the code. It helps you write safer and more reliable code by ensuring that you are using the correct types for your variables, properties, and functions.

In this blog post, we have discussed various examples to demonstrate narrowing, including basic narrowing, user-defined type guards, and discriminated unions. By understanding and utilizing these concepts, you can improve the safety and reliability of your TypeScript code.

As you continue learning and working with TypeScript, you will encounter more advanced concepts related to narrowing and type checking. Keep practicing and experimenting with different code examples to strengthen your understanding of TypeScript and its features.

Happy coding!