Фильтрация типов значений TypeScript
TypeScript — бесценный инструмент для написания безопасного кода. Он отлавливает ошибки на ранней стадии и предупреждает о проблемах, которые могут вызвать исключения во время выполнения. TypeScript вычисляет тип данных, с которыми вы работаете, и избавляет вас от необходимости писать множество явных аннотаций типов в коде.
Иногда бывают случаи, когда TypeScript неправильно определяет значение, и это часто происходит при фильтрации массива, содержащего данные разных типов. Это может привести к тому, что TypeScript выдаёт предупреждения для валидного кода, что может сбить с толку.
В этом руководстве мы отфильтруем массив, содержащий данные смешанных типов, с помощью метода filter()
и убедимся, что TypeScript правильно выводит значения.
Необходимые условия
Чтобы следовать этому руководству, вам понадобятся:
- TypeScript, установленный в вашей системе.
- Чтобы узнать, как его установить, посетите страницу документации. Если вы не можете или не хотите его устанавливать, вы можете воспользоваться TypeScript playground.
- Знакомство с интерфейсами и защитой типов в TypeScript
- Хороший текстовый редактор, который может показать вам ошибки перед компиляцией, например VS Code, имеющий встроенную поддержку TypeScript
Как фильтровать элементы в TypeScript
Одна из самых распространённых задач в программировании — фильтрация массива. JavaScript поставляется с методом filter()
, фильтрующий массив на основе заданных критериев или условий и возвращает новый массив только с теми элементами, которые удовлетворяют заданному условию.
Предположим, что у вас есть массив с числами:
const numbers = [10, 15, 20, 30, 40];
Если вы хотите создать новый массив, содержащий только числа больше 20, вы можете отфильтровать элементы следующим образом:
const greaterThanTwenty = numbers.filter((number) => {
return number > 20;
});
console.log(greaterThanTwenty);
// результат
[30, 40]
Метод filter()
принимает обратный вызов, который проверяет, больше ли число чем 20, и возвращает true
, если условие выполнено. Затем метод возвращает массив, содержащий только те элементы, которые удовлетворяют условию.
TypeScript может определить тип нового массива, возвращаемого методом filter()
, при наведении курсора на переменную greaterThanTwenty
в редакторе или с помощью TypeScript playground:
Ниже приведён ещё один пример, который работает со сложными данными. В нем мы определяем интерфейс Doctor
для представления объектов, которые находятся в массиве doctors
. Затем мы используем метод filter()
для возврата объектов, у которых свойство specialty
установлено на 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"
);
Наведя курсор на переменную cardiologists
, мы заметим, что TypeScript определяет, что массив содержит объекты, соответствующие интерфейсу Doctor
:
Как мы видели, TypeScript может вычислять типы при использовании метода filter()
без каких-либо проблем — потому что массивы, которые мы рассматривали до сих пор, содержат элементы одного типа. В следующем разделе мы рассмотрим фильтрацию массива с элементами разных типов.
Фильтрация массива элементов различных типов в TypeScript
В этом разделе мы создадим небольшую программу, содержащую массив элементов разных типов, чтобы увидеть проблемы, которые возникнут при выводе TypeScript.
Для начала создадим два интерфейса, Doctor
и Nurse
:
interface Doctor {
type: "DOCTOR";
name: string;
specialty: string;
}
interface Nurse {
type: "NURSE";
name: string;
nursingLicenseNumber: string;
}
type MedicalStaff = Doctor | Nurse;
В примере вы определяете два интерфейса, Doctor
и Nurse
, для представления объектов, которые будут находиться в массиве, который мы скоро определим. Интерфейс Doctor
представляет объект с полями type
, name
и specialty
; интерфейс Nurse
имеет поля type
, name
и nursingLicenseNumber
.
Для хранения объектов, которые могут быть представлены интерфейсом Doctor
или Nurse
, в последней строке мы определяем тип объединения MedicalStaff
.
Далее создадим массив, содержащий объекты, соответствующие типу объединения 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];
Мы создали массив staffMembers
, который содержит четыре объекта. Первые два объекта представлены интерфейсом Doctor
, а два других — интерфейсом Nurse
.
После этого отфильтруйте объекты, у которых нет свойства nursingLicenseNumber
:
...
const nurses = staffMembers.filter((staff) => "nursingLicenseNumber" in staff);
const nursingLicenseNumbers = nurses.map((nurse) => nurse.nursingLicenseNumber);
console.log(nursingLicenseNumbers);
В первой строке мы отфильтровываем все объекты, не имеющие свойства nursingLicenseNumber
, которые являются объектами, соответствующими интерфейсу Doctor
. Далее мы используем метод map()
, чтобы вернуть только номера сестринских лицензий в переменной nursingLicenseNumbers
.
После написания кода вы заметите предупреждение:
При ближайшем рассмотрении предупреждение выглядит следующим образом:
// Error
Property 'nursingLicenseNumber' does not exist on type 'Doctor | Nurse'.
Property 'nursingLicenseNumber' does not exist on type 'Doctor'.
Это происходит потому, что предполагаемый тип переменной nurses - const nurses: (Doctor | Nurse)[]
, хотя метод filter()
удаляет объекты без свойства nursingLicenseProperty
. Что ещё хуже, если навести курсор на свойство nursingLicenseNumbers
, TypeScript определяет его как const nursingLicenseNumbers: any[]
.
В идеальном случае переменная nurses
должна определяться как const nurses: Nurse[]
. Таким образом, TypeScript не будет отмечать nurse.nursingLicenseNumber
в методе map()
.
Фильтрация с использованием защиты пользовательского типа с предикатом типа
До сих пор мы узнали, что TypeScript отменяет флаг nurse.nursingLicenseNumber
, поскольку это свойство недоступно в интерфейсе Doctor
. Чтобы обойти эту ошибку, нам нужно использовать защиту пользовательских типов с предикатом типа.
Пользовательская защита типа может проверять практически любой тип, который мы определяем сами, в отличие от других защит типа, таких как typeof
, ограниченных несколькими встроенными типами.
В нашем примере добавьте предикат типа staff is Nurse
к методу filter()
в качестве явной аннотации возвращаемого типа:
const nurses = staffMembers.filter(
(staff): staff is Nurse => "nursingLicenseNumber" in staff
);
const nursingLicenseNumbers = nurses.map((nurse) => nurse.nursingLicenseNumber);
console.log(nursingLicenseNumbers);
Предикат типа staff is Nurse
сообщает TypeScript, что функция, переданная в метод filter()
, будет иметь тип Nurse
.
Если теперь навести курсор на переменную nurses
, TypeScript примет её за :Nurse[]:
const nurses: Nurse[] = staffMembers.filter(
(staff): staff is Nurse => "nursingLicenseNumber" in staff
);
И когда вы наведёте курсор на nursingLicenseNumbers
, TypeScript правильно определит его как string[]
вместо any[]
:
const nursingLicenseNumbers: string[] = nurses.map((nurse) => nurse.nursingLicenseNumber);
Другим рекомендуемым способом решения этой проблемы является определение функции защиты типа. Функция защиты типа имеет:
- Возвращаемое значение, которое оценивается как логическое
- Предикат типа
В следующем коде используется функция защиты типа isStaff
:
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)
В примере вы определяете функцию защиты типа isStaff()
из функции filter()
в предыдущем примере в функцию isStaff
. Затем вы вызываете filter()
с функцией isStaff()
в качестве аргумента.
Вы можете повторно использовать функцию isStaff()
в других местах, где требуется аналогичный предикат типа.
Если вы хотите получить доступ к свойствам, специфичным для типа MedicalStaff
, вы можете использовать предикат типа isStaff
в операторе if
:
const surgicalNurse: MedicalStaff = {
type: "NURSE",
name: "Jane Doe",
nursingLicenseNumber: "RN479312",
};
if (isStaff(surgicalNurse)) {
console.log(surgicalNurse.nursingLicenseNumber);
}
TypeScript сузит тип до типа Nurse
. Вы можете убедиться в этом, наведя курсор на переменную surgicalNurse
:
Проблемы с защитой пользовательских типов
Защитник пользовательских типов может решить множество проблем при фильтрации массива смешанных типов. Однако это не безошибочное решение, и у него есть проблема, о которой нужно знать, чтобы использовать его наилучшим образом.
Проблема заключается в том, что TypeScript не заботится о правильности реализации. Это может дать вам ложное чувство уверенности в том, что вы пишете безопасный код.
Возьмём следующий пример:
const isStaff = (staff: any) : staff is Nurse => true;
const nurses = staffMembers.filter(isStaff);
const nursingLicenseNumbers = nurses.map(nurse => nurse.nursingLicenseNumber);
console.log(nursingLicenseNumbers)
Функция isStaff()
не проверяет, имеет ли какой-либо из объектов в массиве свойство nursingLicenseNumber
, и при этом TypeScript не предупреждает нас. Это вводит нас в заблуждение, заставляя поверить в правильность реализации.
Это может выглядеть как ошибка в TypeScript, но это выбор разработчиков, который можно найти в выпуске TypeScript на GitHub.
Чтобы убедиться в безопасности кода, следует просмотреть реализацию функций защиты типов, чтобы убедиться, что они реализованы правильно, поскольку в TypeScript нет встроенного решения, которое могло бы отметить эту проблему.
Заключение
В этой статье вы узнали, как фильтровать значения в массиве с помощью TypeScript. Сначала вы узнали, как фильтровать массив с элементами одинакового типа. Затем вы перешли к фильтрации массива, содержащего элементы смешанных типов, что вызвало проблемы с выводом. После этого мы использовали защитник пользовательских типов, чтобы помочь TypeScript правильно вывести массив смешанных типов во время фильтрации. Чтобы продолжить знакомство с TypeScript, посетите архив нашего блога.