6 ways to narrow types in TypeScript
In a TypeScript program, a variable can move from a less precise type to a more precise type. This process is called type narrowing. We can use type narrowing to avoid type errors like the one below:
function addLeg(animal: Animal) {
animal.legs = animal.legs + 1; // 💥 - Object is possibly 'undefined'
}
In this post, we are going to cover 6 different ways we can narrow types in TypeScript.
Using a conditional value check
The Animal
type in the above example is as follows:
type Animal = {
name: string;
legs?: number;
};
TypeScript raises a type error because the legs
property in the addLeg
function because it could be undefined
, and it doesn’t make sense to add 1
to undefined
.
A solution is to check whether legs
is truthy before it is incremented:
function addLeg(animal: Animal) {
if (animal.legs) { animal.legs = animal.legs + 1;
}}
legs
is of type number | undefined
before the if
statement and is narrowed to number
within the if
statement. This type narrowing resolves the type error.
This approach is useful for removing null
or undefined
from types or removing literals from union types.
Using a typeof
type guard
Consider the following function which duplicates the parameter if it is a string
or doubles it if it is a number
:
function double(item: string | number) {
if (typeof item === "string") {
return item.concat(item); // item is of type string
} else {
return item + item; // item is of type number
}
}
The item
parameter is of type number
or string
before the if
statement. Within the if
branch, item
is narrowed to string
, and within the else
branch, item
is narrowed to number
.
This pattern is called a typeof
type guard and is useful for narrowing union types of primitive types.
Using an instanceof
type guard
Lots of the types we use are more complex than primitive types though. Consider the following types:
class Person {
constructor(
public firstName: string,
public surname: string
) {}
}
class Organisation {
constructor(public name: string) {}
}
type Contact = Person | Organisation;
The following function raises a type error:
function sayHello(contact: Contact) {
console.log("Hello " + contact.firstName);
// 💥 - Property 'firstName' does not exist on type 'Contact'.
}
This is because contact
might be of type Organisation
, which doesn’t have a firstName
property.
An instanceof
type guard can be used with class types as follows:
function sayHello(contact: Contact) {
if (contact instanceof Person) { console.log("Hello " + contact.firstName);
}}
The type of contact
is narrowed to Person
within the if
statement, which resolves the type error.
Using an in
type guard
We don’t always use classes to represent types though. The types in the last example could be as follows:
interface Person {
firstName: string;
surname: string;
}
interface Organisation {
name: string;
}
type Contact = Person | Organisation;
The instanceof
type guard doesn’t work with the interfaces or type aliases. Instead we can use an in
operator type guard:
function sayHello(contact: Contact) {
if ("firstName" in contact) { console.log("Hello " + contact.firstName);
}}
The type of contact
is narrowed to Person
within the if
statement, which means no type error occurs.
Using a type guard function with a type predicate
Consider the following example:
type Rating = 1 | 2 | 3 | 4 | 5;
async function getRating(productId: string) {
const response = await fetch(
`/products/${productId}`
);
const product = await response.json();
const rating = product.rating;
return rating;
}
The return type of the function is inferred to be Promise<any>
. So, we if assign a variable to the result of a call to this function, it will have the any
type:
const rating = await getRating("1"); // type of rating is `any`
This means that no type checking will occur on this variable. 😞
We can use a type guard function that has a type predicate to make this code more type-safe. Here is the type guard function:
function isValidRating(
rating: any
): rating is Rating {
if (!rating || typeof rating !== "number") {
return false;
}
return (
rating === 1 ||
rating === 2 ||
rating === 3 ||
rating === 4 ||
rating === 5
);
}
rating is Rating
is the type predicate in the above function.
A type guard function must return a boolean value if a type predicate is used.
We can use this type guard function in our example code as follows:
async function getRating(productId: string) {
const response = await fetch(
`/products/${productId}`
);
const product = await response.json(); const rating = product.rating;
if (isValidRating(rating)) {
return rating; // type of rating is `Rating`
} else {
return undefined;
}
}
The type of rating
inside the if
statement is now Rating
.
Nice! 😀
Using a type guard function with an assertion signature
Continuing on with the rating example, we may want to structure our code a little differently:
async function getRating(productId: string) {
const response = await fetch(
`/products/${productId}`
);
const product = await response.json(); const rating = product.rating;
checkValidRating(rating); // should throw error if invalid rating
return rating; // type should be narrowed to `Rating`
}
Here we want the checkValidRating
function to throw an error if the rating is invalid.
We also want the type of rating
to be narrowed to Rating
after checkValidRating
has been successfully executed.
We can use a type guard function with an assertion signature to do this:
function checkValidRating(
rating: any
): asserts rating is Rating {
if (!rating || typeof rating !== "number") {
throw new Error("Not a rating");
}
if (
rating !== 1 &&
rating !== 2 &&
rating !== 3 &&
rating !== 4 &&
rating !== 5
) {
throw new Error("Not a rating");
}
}
asserts rating is Rating
is the assertion signature in the above type guard function. If the function returns without an error being raised, then the rating
parameter is asserted to be of type Rating
.
In getRating
, the rating
variable is of type Rating
after the call to checkValidRating
. 😀
Wrap up
TypeScript automatically narrows the type of a variable in conditional branches. Doing a truthly condition check will remove null
and undefined
from a type. A typeof
type guard is a great way to narrow a union of primitive types. The instanceof
type guard is useful for narrowing class types. The in
type guard is an excellent way of narrowing object types. Function type guards are helpful in more complex scenarios where the variable’s value needs to be checked to establish its type.
Did you find this post useful?
Let me know by sharing it on Twitter.If you to learn more about TypeScript, you may find my free TypeScript course useful: