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!