Фильтрация типов значений TypeScript

Источник: «Filtering TypeScript value types»
Узнайте, как работают фильтры TypeScript, включая фильтры массивов и типов значений, а также как использовать и избегать проблем с защитой типов.

TypeScript — бесценный инструмент для написания безопасного кода. Он отлавливает ошибки на ранней стадии и предупреждает о проблемах, которые могут вызвать исключения во время выполнения. TypeScript вычисляет тип данных, с которыми вы работаете, и избавляет вас от необходимости писать множество явных аннотаций типов в коде.

Иногда бывают случаи, когда TypeScript неправильно определяет значение, и это часто происходит при фильтрации массива, содержащего данные разных типов. Это может привести к тому, что TypeScript выдаёт предупреждения для валидного кода, что может сбить с толку.

В этом руководстве мы отфильтруем массив, содержащий данные смешанных типов, с помощью метода filter() и убедимся, что 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:

TypeScript воспринимает переменную как number[]

Ниже приведён ещё один пример, который работает со сложными данными. В нем мы определяем интерфейс 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 определяет переменную как тип 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.

После написания кода вы заметите предупреждение:

Снимок экрана предупреждения TypeScript свойства nursingLicenseNumber

При ближайшем рассмотрении предупреждение выглядит следующим образом:

// 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:

Наведите курсор на переменную 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, посетите архив нашего блога.

Дополнительные материалы

Предыдущая Статья

Тестирование правил валидации Laravel с помощью Pest PHP

Следующая Статья

Автоматическая генерация RSS лент в приложении Laravel