Перейти к содержанию

Обобщённые функции

Функция чтения первого элемента может работать и с курсами, и с названиями. Если написать её специально для Course[], потребуется другая функция для строк. Если заменить контракт на произвольное значение, вызывающая сторона потеряет информацию о том, что именно получила. Обобщённая функция сохраняет связь входного и выходного типов.

Параметр типа обозначим T. Он не является значением, которое передаётся в браузере; это место для типа элемента конкретного вызова. В нашем каталоге операция остаётся маленькой: прочитать нулевой элемент, если он существует. Именно небольшой пример помогает увидеть полезность обобщения без сложной архитектуры и без повторного изучения массивов JavaScript.

Связь двух сторон функции

В самостоятельном снимке lesson-09 модель, readonly-данные и настройки предыдущего урока сохраняются. Полный src/app.ts:

import { courses } from './data.js';

function first<T>(items: readonly T[]): T | undefined {
  return items[0];
}

const course = first(courses);
const title = first(courses.map(item => item.title));
const absent = first<string>([]);
console.log(course?.title ?? 'Нет первого курса');
console.log(title?.toUpperCase() ?? 'Нет названия');
console.log(absent === undefined ? 'Пустой список' : absent);

Ожидаются строки «Современный JavaScript», «СОВРЕМЕННЫЙ JAVASCRIPT» и «Пустой список». Для первого вызова T связан с Course, для второго — со строкой. Поэтому после проверки отсутствия вызывающая сторона может читать поля курса или использовать строковый метод соответственно.

Возвращаемый тип не просто T, а T | undefined. Обобщение сохраняет тип элемента, но не доказывает наличие хотя бы одного элемента. Вход — массив произвольной длины. Поэтому пустой набор остаётся обычным допустимым аргументом, а результат требует обработки.

Мы используем readonly T[], поскольку чтению не нужна возможность изменять коллекцию. Функция допускает массивы для чтения и обычные изменяемые массивы. Тип элемента остаётся параметром, а ограничения структуры массива уже известны независимо от него: можно читать индекс и длину, но нельзя предполагать свойства конкретного элемента.

Вызов first<string>([]) явно задаёт тип будущего элемента пустого набора. Во многих непустых вызовах инструмент выводит аргумент типа сам. Явные угловые скобки полезны, когда источник не содержит достаточно информации или когда автор хочет закрепить договор, но не нужны после каждого имени функции.

Почему не any или unknown

Если функция объявлена как принимающая any[] и возвращающая any, внешне она станет более свободной. Однако после вызова инструмент разрешит почти любые операции с результатом, включая ошибочные. Такая свобода не является сохранением знания: связь с исходным элементом потеряна, а проверка вызывающего кода ослаблена.

unknown честнее признаёт незнание, но тоже не сохраняет конкретный тип. Возвращая unknown, функция вынудила бы каждый вызов заново доказывать, что результат является курсом или строкой. При работе с уже проверенными локальными данными это лишняя потеря информации.

Обобщённый T занимает другое место. Функция не знает частный тип заранее, зато обещает вернуть элемент именно того типа, который получила в массиве. Вызывающая сторона знает частный тип и сохраняет его. Поэтому можно писать общую реализацию, не отключая полезную проверку результата.

Это не автоматическая runtime-проверка элемента. Если вызвать функцию из непроверенного JavaScript с неожиданными данными, она вернёт фактическое значение. Внешний JSON сначала проходит парсер шестого урока. Обобщение поддерживает уже установленный договор, а не заменяет входную схему.

Тело должно работать для всех T

Внутри first нельзя без основания обратиться к items[0].title. Даже если первый наш вызов использует курсы, следующий использует строки. Реализация должна соответствовать каждому допустимому T, а не тайно рассчитывать на один любимый вариант.

Рассмотрим неправильный фрагмент для чтения:

// Не включайте в рабочий app.ts: у произвольного T нет обещанного title.
function brokenFirst<T>(items: readonly T[]): string {
  const item = items[0];
  return item.title;
}

Здесь две независимые причины проблемы: элемента может не быть, а его тип не обещает title. Проверка item !== undefined решит только первую. Для второй потребуется либо ограничение параметра типа, либо специальная операция над курсами. В следующем уроке мы введём ограничение для индексирования по id.

Не следует чинить такой код утверждением item as Course. Оно делает обобщённость фиктивной: функция принимает любой тип, но реализация фактически требует курс. Подобные несоответствия особенно неприятны для библиотек, где вызывающий автор не видит внутреннего приведения.

Если операция действительно форматирует название курса, достаточно обычного параметра Course. Обобщение оправдано сохранением связи или полезной вариативностью, а не добавлением буквы T ради современного вида. Для нашей функции связь прозрачна: массив элементов данного типа превращается в один элемент того же типа либо отсутствие.

Обобщение не создаёт копию

Функция возвращает ссылку на первый объект массива. Она не клонирует курс и не преобразует его поля. Поэтому последующие действия с объектом подчиняются тому же договору readonly и владения, который был у исходного элемента. Строковый результат, напротив, является примитивным значением; эти свойства определяются JavaScript, а не буквой параметра типа.

Если вместо нулевого индекса выбрать последний, тип результата останется T | undefined, но реализация и смысл операции изменятся. Если выбрать все подходящие элементы, естественным результатом будет массив T[]. Во всех случаях сначала определяем поведение, затем выражаем его отношения, а не начинаем с максимально сложного универсального типа.

Смешанный массив сохраняет свою неоднородность

Обобщение не делает неоднородные данные одинаковыми. Если вход содержит число и строку, тип элемента может включать оба варианта. Тогда first вернёт число или строку либо отсутствие. Вызывающая сторона должна определить вариант перед числовым или строковым методом. Точность связи состоит в сохранении исходного знания, а не в обещании самого удобного частного типа.

Если программа всегда ожидает курсы, неоднородный вход — повод проверить границу модели, а не расширить каждую локальную функцию до произвольного значения. После parseCourses массив уже содержит Course, поэтому обобщённое чтение первого элемента сохраняет именно этот результат.

Обратите внимание и на пустой массив, явно переданный как first<Course>([]). Возвращаемое отсутствие нормально, даже если аргумент типа указан. Угловые скобки определили, какими были бы элементы; они не добавили ни одного элемента при выполнении. Поэтому optional chaining или отдельная ветка остаются необходимыми.

Для поиска подходящего элемента потребовался бы другой аргумент — предикат. Его тип можно связать с тем же T, но не стоит добавлять этот механизм в простое чтение первого значения только ради универсальности. Небольшая операция легче проверяется чтением, когда имя, подпись и тело описывают одно действие. Сначала используйте её на курсе и строке, прослеживая связь, а дальнейшее расширение связывайте с конкретной потребностью программы, а не с возможностью записать ещё один параметр типа.

Связь аргумента и результата объясняется в Generics. В нашей серии важен конкретный эффект: после одного общего вызова доступен курс, после другого — строка, а пустой набор не скрыт. Исходник при подготовке не запускался, ожидаемые строки следуют из первой записи фиксированного массива.

Попробуйте глазами проследить вариант first([20, 16, 12]). Результат будет числом или отсутствием, поэтому чтение title лишено основания, а числовое форматирование возможно после проверки. В следующем уроке будем индексировать записи с обязательным id и выразим это требование, сохранив остальные свойства частного типа.

Оглавление курса.