Типизация функций
После проверки внешнего массива остальная программа может работать с внутренней моделью Course. Это позволяет разделить обязанности: одна функция отвечает за вход, другая выбирает курсы, третья подсчитывает число уроков. Чтобы соединение оставалось понятным, опишем параметры и результаты функций.
В этом уроке мы не меняем алгоритм фильтрации JavaScript. Новый результат — контракт операции и передаваемого callback. Функция отбора будет принимать любую подходящую проверку курса, а подсчёт всегда вернёт число. Четыре записи, темы и сумма 60 сохраняются, поэтому влияние новых типов можно отделить от изменения данных.
Подпись операции
У функции есть входные значения и возвращаемое значение. Тип параметра определяет, что вызывающая сторона должна предоставить, а тип результата — на что вправе рассчитывать после вызова. Подпись не говорит обо всех смысловых правилах: например, число результата ещё не объясняет, что оно является суммой уроков. Названия операции и переменных дополняют типы.
В src/model.ts после ранее созданного парсера добавьте полные определения:
export type CoursePredicate = (course: Course) => boolean;
export function selectCourses(
courses: readonly Course[], accept: CoursePredicate
): Course[] {
return courses.filter(accept);
}
export function countLessons(courses: readonly Course[]): number {
return courses.reduce((total, course) => total + course.lessons, 0);
}
CoursePredicate — тип функции, которая получает курс и возвращает логическое решение. Это описание функции как значения: её можно передать другой операции. Само объявление не вызывает callback и не создаёт готовую проверку конкретной темы.
selectCourses принимает массив для чтения и callback accept. Использование readonly Course[] означает, что функция не обещает изменять структуру входного массива через этот параметр. Оно не замораживает данные в памяти; подробно различие разберём в следующем уроке. Результат — новый обычный массив Course[], созданный методом filter.
countLessons возвращает число. Внутри известны тип элемента и числовое поле, поэтому callback reduce не требует отдельного описания каждого имени. Начальный ноль согласован с результатом для пустого набора: сумма отсутствующих уроков равна нулю. Это осмысленная политика функции, а не значение, случайно выбранное ради удобного типа.
Callback получает контекст
Полный src/app.ts:
import { courses } from './data.js';
import { selectCourses, countLessons } from './model.js';
import type { CoursePredicate } from './model.js';
const frontend: CoursePredicate = course => course.topic === 'frontend';
const selected = selectCourses(courses, frontend);
console.log(`Курсов: ${selected.length}. Уроков: ${countLessons(selected)}.`);
console.log(`Всего уроков: ${countLessons(courses)}`);
Ожидаемый вывод:
Курсов: 3. Уроков: 48.
Всего уроков: 60
У переменной frontend явно записан тип CoursePredicate. Поэтому инструмент знает форму параметра course стрелочной функции без повторной аннотации. Возвращаемое сравнение имеет логический результат. Эта проверка соответствует контракту и может быть передана в отбор.
Попробуйте мысленно заменить тело на course.title. Получится строка, а наш объявленный callback обещает логическое решение. Обычный JavaScript-фильтр может интерпретировать истинность строк, но в модели серии мы сознательно требуем boolean: это делает правила выбора явными. Чтобы выбрать непустое название, следует написать сравнение или другую настоящую логическую проверку.
Другой callback можно создать как course => course.lessons >= 16. Он выберет JavaScript и HTML/CSS, то есть два курса и 36 уроков. Данные сохраняются, изменяется только условие. Так видно, зачем операция принимает функцию: общий механизм обхода не смешан с одним конкретным бизнес-правилом.
Контракт результата и тело функции
Аннотация возвращаемого значения особенно полезна, когда функция имеет несколько веток. Если одна ветка вернёт массив, а другая забывает возврат, вызывающий код уже не получает обещанный результат. В такой ситуации стоит исправить смысл ветки, а не поспешно расширять возвращаемый тип до неопределённого значения.
Для сравнения можно добавить самостоятельную функцию после импортов:
import type { Course } from './model.js';
function summarizeTitle(course: Course, prefix = 'Курс'): string {
return `${prefix}: ${course.title}`;
}
const course = courses[0];
if (course !== undefined) console.log(summarizeTitle(course));
else console.log('Курс не найден');
Этот фрагмент показывает строковый параметр со значением по умолчанию. Ожидается «Курс: Современный JavaScript». Если первый курс отсутствует, отдельная ветка сообщает об этом; искусственная запись в каталог не добавляется. Функция форматирования принимает уже найденный объект и не отвечает за его поиск. Фрагмент иллюстрирует дополнительный вызов и не включён в рабочий снимок.
Более практичный вариант — функция, принимающая уже найденный Course. Тогда выбор политики отсутствия остаётся у вызывающей стороны. Это уменьшает число обязанностей форматирования: функция не ищет курс, не загружает JSON и не решает, допустима ли пустая выборка.
Заметьте также разницу между отсутствующим аргументом и аргументом undefined. Параметр со значением по умолчанию получает запасное значение в обоих случаях. Если контракт требует различать эти случаи, обычного default недостаточно. В текущем каталоге такой необходимости нет, поэтому не усложняем сигнатуру заранее.
Что значит функция без результата
Некоторые операции выполняют действие и не предоставляют полезного возвращаемого значения. Для них существует void, например у callback уведомления. Этот тип описывает договор вызывающей стороны: результат не используется. Он не гарантирует, что любая переданная реализация физически никогда ничего не вернёт.
Например, callback, который вызывает console.log, нужен ради сообщения. Его не следует использовать вместо CoursePredicate, поскольку отбор нуждается в логическом решении. Типы функций помогают заметить такую ошибку назначения даже тогда, когда обе операции имеют по одному параметру с похожим названием.
В нашей серии не нужны перегрузки ради каждого режима фильтра. Возвращаемая форма всегда одинаковая: массив курсов. Добавлять несколько объявлений только для перечисления frontend, publishing и all означало бы повторить набор, который уже выражается типом параметра. Перегрузки полезны там, где входы и результаты действительно различаются, а не как украшение сигнатуры.
Предикат остаётся отдельным правилом
Можно передать функцию, которая выбирает все курсы: course => true. Можно передать функцию, которая не выбирает ничего. В первом случае ожидаются четыре записи и сумма 60, во втором — пустой массив и сумма ноль. Оба callback соответствуют одному договору, поэтому функция отбора не должна объявлять пустой результат ошибкой без дополнительного требования продукта.
Важна также предсказуемость callback. Если он изменяет внешнюю переменную или сам курс при каждом вызове, тип CoursePredicate не описывает такие побочные эффекты. Подпись гарантирует форму аргумента и результата, а правило чистоты остаётся договором реализации. Для учебного каталога мы используем сравнение, которое не изменяет данные.
Порядок вызовов определяется методом filter и текущим порядком массива, а не типом функции. Если в будущем вы сортируете данные перед отбором, состав выбранных записей по теме останется тем же, но порядок может измениться. Сумма уроков не зависит от этого порядка. Поэтому полезно отдельно держать правила выбора и представления.
Читая новую подпись, сначала выясните, кто владеет входными данными и что обещает результат. Здесь отбор создаёт новый массив ссылок на прежние объекты, а подсчёт создаёт число. Ни одна операция не производит глубокую копию курсов и не сохраняет их на сервере. Такая конкретность помогает применять функции повторно, не ожидать скрытых эффектов и не добавлять ненужные проверки формы в каждую локальную арифметическую операцию.
Функциональные типы и параметры подробно рассмотрены в More on Functions. Для учебного каталога важны понятный callback и честный числовой результат. Код подготовлен без выполнения; ожидаемые 48 и 60 получены из зафиксированных чисел четырёх курсов.
В следующем уроке уточним обещание «читать входной массив»: отделим структуру массива, свойства элемента и фиксированный порядок значений сводки. Это позволит сортировать представление, сохраняя исходный каталог для других операций.