TypeScript is an invaluable tool for writing safe code. It catches bugs early and provides warnings for issues that can cause exceptions during runtime. TypeScript will infer the type of data you are working with and save you from writing a lot of explicit type annotations in your code.
Sometimes, there are cases when TypeScript does not infer the value correctly, and this often happens when filtering an array containing data of different types. These can result in TypeScript providing warnings for valid code, which can be confusing.
In this tutorial, we will filter an array containing data of mixed types using the filter()
method and ensure that TypeScript infers the values properly.
Jump ahead:
To follow this tutorial, you will need:
One of the most common tasks in programming is filtering an array. JavaScript comes with the filter()
method that filters an array based on the given criteria or condition and returns a new array with only the elements that passed the condition given.
Assuming you have an array with numbers:
const numbers = [10, 15, 20, 30, 40];
If you want to create a new array with only numbers greater than 20, you can filter the elements as follows:
const greaterThanTwenty = numbers.filter((number) => { return number > 20; }); console.log(greaterThanTwenty); // output [30, 40]
The filter()
method takes a callback that checks if a number is greater than 20
, and that returns true
if the condition is satisfied. The method then returns an array containing only the elements that satisfied the condition.
TypeScript can infer the type of the new array returned by the filter()
method when you hover on the greaterThanTwenty
variable in your editor or using the TypeScript playground:
Below is another example that handles complex data. In it, we define a Doctor
interface to represent objects that are in the doctors
array. We then use the filter()
method to return objects that have a specialty
property set to Cardiology
:
interface Doctor { type: "DOCTOR"; name: string; specialty: string; } const doctors: Doctor[] = [ { type: "DOCTOR", name: "John Doe", specialty: "Dermatology" }, { type: "DOCTOR", name: "Jane Williams", specialty: "Cardiology" }, ]; const cardiologists = doctors.filter( (doctor) => doctor.specialty == "Cardiology" );
When we hover over the cardiologists
variable, we will notice that TypeScript infers that the array contains objects matching the Doctor
interface:
As we have seen, TypeScript can infer types when using the filter()
method without any issues — because the arrays we have looked at so far contain elements of the same type. In the next section, we will look at filtering an array with elements of different types.
In this section, we will create a small program that contains an array of elements of different types to see the issues that will arise during TypeScript inference.
To begin, create two interfaces, Doctor
and Nurse
:
interface Doctor { type: "DOCTOR"; name: string; specialty: string; } interface Nurse { type: "NURSE"; name: string; nursingLicenseNumber: string; } type MedicalStaff = Doctor | Nurse;
In the example, you define two interfaces, Doctor
and Nurse
, to represent objects that will be in the array that we will soon define. The Doctor
interface represents an object with type
, name
, and specialty
fields; the Nurse
interface has fields type
, name
, and nursingLicenseNumber
.
To store objects that can be represented by either the Doctor
or Nurse
interface, we define a union type MedicalStaff
in the last line.
Next, create an array containing objects that match the union type MedicalStaff
:
... const doctor1: MedicalStaff = { type: "DOCTOR", name: "John Doe", specialty: "Dermatology", }; const doctor2: MedicalStaff = { type: "DOCTOR", name: "Jane Williams", specialty: "Cardiology", }; const nurse1: MedicalStaff = { type: "NURSE", name: "Bob Smith", nursingLicenseNumber: "RN123456", }; const nurse2: MedicalStaff = { type: "NURSE", name: "Alice Johnson", nursingLicenseNumber: "RN654321", }; const staffMembers = [doctor1, doctor2, nurse1, nurse2];
We created a staffMembers
array, which contains four objects. The first two objects are represented by the Doctor
interface, while the other two are represented by the Nurse
interface.
Following this, filter out the objects that don’t have the nursingLicenseNumber
property:
... const nurses = staffMembers.filter((staff) => "nursingLicenseNumber" in staff); const nursingLicenseNumbers = nurses.map((nurse) => nurse.nursingLicenseNumber); console.log(nursingLicenseNumbers);
In the first line, we filter out all the objects that don’t have a nursingLicenseNumber
property, which are the objects corresponding to the Doctor
interface. Next, we use the map()
method to return only the nursing license numbers in the nursingLicenseNumbers
variable.
After writing the code, you will notice a warning:
Upon close inspection, the warning is similar to the following:
// Error Property 'nursingLicenseNumber' does not exist on type 'Doctor | Nurse'. Property 'nursingLicenseNumber' does not exist on type 'Doctor'.
This is because the inferred type for the nurses
variable is const nurses: (Doctor | Nurse)[]
, even though the filter()
method removes the objects without the nursingLicenseProperty
. To make matters worse, if you hover over the nursingLicenseNumbers
property, TypeScript infers it as const nursingLicenseNumbers: any[]
.
In ideal cases, the nurses
variable should be inferred as const nurses: Nurse[]
. That way, TypeScript wouldn’t flag nurse.nursingLicenseNumber
in the map()
method.
So far, we’ve learned TypeScript flags nurse.nursingLicenseNumber
because the property is not available in the Doctor
interface. To get around this error, we need to use a custom type guard with a type predicate.
A custom type guard can check for almost any type we define ourselves, in comparison to other type guards, such as typeof
, which are limited to a few built-in types.
In our example, add a type predicate of staff is Nurse
to the filter()
method as an explicit return type annotation:
const nurses = staffMembers.filter( (staff): staff is Nurse => "nursingLicenseNumber" in staff ); const nursingLicenseNumbers = nurses.map((nurse) => nurse.nursingLicenseNumber); console.log(nursingLicenseNumbers);
The type predicate staff is Nurse
tells TypeScript that that function passed to the filter()
method will be of type Nurse
.
If you hover over the nurses
variable now, TypeScript will infer it as :Nurse[]
:
const nurses: Nurse[] = staffMembers.filter( (staff): staff is Nurse => "nursingLicenseNumber" in staff );
And when you hover over the nursingLicenseNumbers
, TypeScript will infer it properly as string[]
instead of any[]
:
const nursingLicenseNumbers: string[] = nurses.map((nurse) => nurse.nursingLicenseNumber);
Another recommended way to handle this is to define a type guard function. A type guard function has:
The following code uses a isStaff
type guard function:
const isStaff = (staff: MedicalStaff): staff is Nurse => "nursingLicenseNumber" in staff; const nurses = staffMembers.filter(isStaff); const nursingLicenseNumbers = nurses.map((nurse) => nurse.nursingLicenseNumber); console.log(nursingLicenseNumbers)
In the example, you define the isStaff()
type guard function from the filter()
function in the previous example to the isStaff
function. You then call filter()
with the isStaff()
function as an argument.
You can reuse the isStaff()
function in other places that need a similar type predicate.
If you want to access properties that are specific to the MedicalStaff
type, you can use the isStaff
type guard in an if
statement:
const surgicalNurse: MedicalStaff = { type: "NURSE", name: "Jane Doe", nursingLicenseNumber: "RN479312", }; if (isStaff(surgicalNurse)) { console.log(surgicalNurse.nursingLicenseNumber); }
TypeScript will narrow the type down to the Nurse
type. You can confirm that by hovering over the surgicalNurse
variable:
A custom type guard can fix a lot of issues when filtering an array of mixed types. However, it is not an infallible solution and has an issue you need to be aware of to make the best use of it.
The issue is that TypeScript does not care if the implementation is correct. This can give you a false sense of comfort that you are writing safe code.
Take the following example:
const isStaff = (staff: any) : staff is Nurse => true; const nurses = staffMembers.filter(isStaff); const nursingLicenseNumbers = nurses.map(nurse => nurse.nursingLicenseNumber); console.log(nursingLicenseNumbers)
The isStaff()
function doesn’t check if any of the objects in the array has a nursingLicenseNumber
property, and yet TypeScript doesn’t warn us. This misleads us into believing that the implementation is correct.
This may look like a bug in TypeScript, but this is a design choice, which can find in TypeScript’s GitHub issue.
To ensure you have safe code, you should review the type guard function implementation to ensure that they are well implemented because TypeScript doesn’t have a built solution that can flag this issue.
In this tutorial, you learned how to filter values in an array using TypeScript. First, you learned how to filter an array with elements of identical type. You then proceeded to filter an array containing elements of mixed types which brought up inference issues. From there, we used a custom type guard to help TypeScript properly infer an array of mixed types during filtering. To continue your TypeScript journey, visit our blog archives.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Filtering TypeScript value types"
Thankyou, it is very helpful.