Narrowing Types in Typescript
In this issue, I walk you through Type Narrowing, what it is and techniques we can use to do Type narrowing.
Hey, Wycliffe here, and welcome to this week’s mid-week scoop.
At the beginning of every week, I usually send out an article covering various Techniques and Lessons in Typescript to help you learn and build more Type safe code in Typescript, such as How to Overload Functions in Typescript.
Please consider subscribing to this free newsletter if you haven’t and share it with friends if you already have.
Without further ado:
Let’s take the following code:
function square(x: number | undefined) {
return x * x;
}
If you have a keen eye, you are attempting to square x
, you will get an error: x is possibly undefined
:
The question is, how do you square x, without compromising type safety? And this is a common question that I get so frequently, that I am writing about it, for a second time; this time, I will keep very short as my first newsletter issue for 2023.
Back to our function previously, there are two possible solutions:
The first and most common approach I see out in the wild is Type Assertion, AVOID THIS.
function square(x: number | undefined) {
return (x as number) * (x as number);
}
This has a few disadvantages, Assertions basically overrule the TS compiler, basically disabling type checking and compromising type safety, which is not great and goes against the reason we are using Typescript. Second, just because we asserted x to a number, doesn’t mean it won’t be undefined; there is a bug within our code now, which is even worse.
TIP 💡
Avoid type assertions and only use them when you have more information than the Typescript compiler.
The second approach, which I prefer most of the time, is using the control flow to narrow the type of x to be a number.
function square(x: number | undefined) {
if(x) {
return x * x;
}
return undefined
}
In this approach, even if our input is undefined
, it doesn’t introduce a bug in our system, as we are handling that situation without compromising type Safety and Typescript is happy.
And this is what we call Type Narrowing. Type narrowing uses the Typescript/JavaScript control flow logic i.e., if
, switch
case, etc., to move types from broad type definition to narrower/specific types, say a number
or undefined
union in our case, to just number.
The previous function, can be re-written in another way, by checking if x
is undefined and throwing an error, as shown below:
function square(x: number | undefined) {
if(!x) {
throw "Invalid types";
}
return x * x;
}
Since we are now throwing an error when x
is undefined, Typescript is able to use this information and narrow our type for x
.
As you can see, Type narrowing is a very useful concept to understand. The above type of narrowing checks whether a value is Truthy or Falsey and narrows based on whether a value is Truthy or Falsey. It works with null
as well.
function square(x: number | null) {
if(!x) {
throw "Invalid types";
}
return x * x;
}
Now, you may be wondering, what other ways can I narrow types in Typescript, let’s look at a few options:
Type Guards
JavaScript has Type Guards, which allow you to check whether a variable of a certain type, for instance, string, object, number, etc. When we use Type Guards within a control flow, Typescript can use this information to narrow the Type appropriately.
if(typeof param === "string") {
// do something with string value
}
if(typeof param === "number") {
// do something with number value
}
if(typeof param === "bigint") {
// do something with bigint value
}
if(typeof param === "boolean") {
// do something with boolean value
}
if(typeof param === "symbol") {
// do something with symbol value
}
if(typeof param === "function") {
// do something with the function
}
Using Equality Narrowing
The rule is simple here, if two variables are equal, they are of the same type, here is an example to demo that:
function someFunction(x: string | number, y: number) {
if(x === y) {
console.log(x) // number
}
}
Using JavaScript in Operator
JavaScript in operator returns true if a key is found within an object and we can use this information for the purpose of narrowing.
type Circle = {
radius: number;
}
type Rectangle = {
length: number;
height: number;
}
function calculateSquare(shape: Circle | Rectangle) {
if("radius" in shape) {
return 3.147 * shape.radius * shape.radius;
} else {
return shape.height * shape.length;
}
}
Using instanceof Class Narrowing
We can use instanceof
to narrow a variables Type by checking if a variable is an instance of class or not, as shown below:
function dateToString(value: string | Date) {
if(value instanceof Date) {
return value.toISOString();
}
return value;
}
Conclusion
In this issue, we explored Type Narrowing and how we can use it to get more Type Safety and get better Typescript support for Types. We also covered some of the common techniques we can use to narrow Types.
And that’s it from me, I hope you enjoyed ❤️ this issue as much as I did writing it, and I hope you have a fantastic week ahead.
Please consider doing one of the following things if you love this issue.
1. Share it ❤️ - Help me spread word of mouth and help All Things Typescript grow:
2. ✉️ Subscribe to this newsletter. You will receive weekly content like this from All Things Typescript that will help you learn and master Typescript.
3. Consider Sponsoring me on GitHub ❤️; you will be helping me create more free content for the benefit of everyone.
Au revoir 👋🏿.
Hello, Maina
How are you?
Thanks for the article. I've appreciated read it. I have a doubt though.
The "square" function have a bug for the value 0 (zero). So the "if" will catch it, even if its type is "number" and throw an error.
Best regards :)