TypeScript / 39 минут

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

Сохраняйте связь между типом входа и результата.

В процессе Теория: впереди Практика: впереди Тест: впереди
06 / 12
После этой главы
  • Сохраняйте связь между типом входа и результата.
  • Применить тему в задаче «Обобщённые функции» и объяснить граничные случаи.

Около 9 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Сохранение связи между входом и выходом

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

Параметр T существует только для статического анализа. В runtime нет отдельного T, по которому можно выполнить typeof T или выбрать конструктор. Реализация остаётся обычным JavaScript-кодом и может использовать только операции, разрешённые заявленными ограничениями. Generic не является способом узнать форму произвольного внешнего значения без проверки.

TypeScript
function identity<T>(value: T): T { return value; }
const title = identity('JavaScript'); // string
const count = identity(22); // number

Вывод параметра типа

TypeScript часто выводит T из аргумента, поэтому писать identity<string>('JS') каждый раз не требуется. Явный аргумент типа полезен, когда вывести желаемый контракт невозможно или нужно сознательно задать более широкий вариант. Следите, чтобы такая явность не скрывала неправильное предположение о runtime-данных.

TypeScript
function first<T>(values: readonly T[]): T | undefined {
  return values[0];
}
const number = first([10, 20]); // number | undefined

Результат включает отсутствие, потому что пустой массив допустим. Generic не доказывает непустоту массива сам по себе. Если контракт требует непустую последовательность, её можно выразить соответствующим кортежем, но принимающая граница должна действительно обеспечить это правило. Не удаляйте undefined через ! только ради более красивого типа.

Ограничения extends

Ограничение T extends Shape говорит, какие операции допустимы для любого подходящего T. Например, наличие length позволяет читать длину, но не превращает значение автоматически в массив. Строка и объект с числовым length тоже могут соответствовать форме. Не используйте больше свойств, чем обещает ограничение.

TypeScript
function withLength<T extends { length: number }>(value: T): number {
  return value.length;
}
console.log(withLength('JS')); // 2
console.log(withLength([1, 2, 3])); // 3

Удачное ограничение минимально: оно отражает реальные потребности алгоритма и не заставляет потребителя передавать лишние поля. Если generic-параметр встречается только один раз и не выражает полезной связи, возможно, достаточно обычного объектного типа. Не добавляйте обобщение как декоративный знак «продвинутого кода».

Несколько параметров и возвращаемые формы

Generic может связывать элемент массива, ключ объекта и результат callback. Например, mapValues принимает преобразование T→R и возвращает R[]. Параметры типов описывают отношения, которые было бы трудно сохранить произвольным union. Слишком широкий общий тип результата может заставить каждого потребителя заново проверять уже известную связь.

TypeScript
function transform<T, R>(values: readonly T[], map: (value: T) => R): R[] {
  return values.map(map);
}
const lengths = transform(['JS', 'TypeScript'], text => text.length);

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

Типичный ложный generic API-клиента

Запись request<User>(url) иногда выглядит как проверка полученного пользователя, хотя реализация просто делает JSON.parse и утверждение as T. Такой API удобен, но его гарантия основана на доверии вызывающего кода, а не на реальной валидации. Для строгой границы передавайте проверяющую функцию или схему, которая действительно превращает unknown в доказанный T.

TypeScript
type Parser<T> = (value: unknown) => T;
async function decode<T>(raw: unknown, parse: Parser<T>): Promise<T> {
  return parse(raw);
}

В этом примере тип результата связан с проверяющим адаптером, но корректность самого parse остаётся ответственностью его реализации. Нельзя написать предикат, который всегда возвращает true, и считать систему проверенной только потому, что компилятор принял сигнатуру.

Практика последнего элемента

По условию функция возвращает последний элемент массива, а для пустого — null. Сохраните T в сигнатуре и выразите результат как T | null. Подставлять число или строку вместо отсутствующего значения нельзя: это нарушит общий контракт для других типов. Чтение последнего элемента должно учитывать, что индекс length - 1 у пустого массива отрицательный.

Проверьте массив чисел, строк и пустую последовательность. Объясните, какой конкретный T выбран для каждого вызова и где появляется null. Затем предложите версию для readonly-массива и сравните её удобство для потребителя. Хорошее обобщение позволяет расширить круг безопасных входов, сохранив точный и честный тип результата.

Загружаем редактор…
Следующая темаkeyof и индексный доступ

Музыка: Reverie — Scott Buckley · CC BY 4.0