$ sudo teach IT
МОДУЛЬ 6 · УРОК 2

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

Синтаксис <T>, вывод типа аргумента, несколько параметров типа

Цели урока

  • Освоить синтаксис обобщённых функций
  • Понять, как работает вывод типа (type inference)
  • Научиться использовать несколько параметров типа
  • Практические примеры обобщённых функций

Синтаксис обобщённой функции

В предыдущем уроке мы узнали, зачем нужны generics. Теперь давайте разберём детально, как их писать. Синтаксис обобщённой функции состоит из нескольких частей:

// Полный синтаксис обобщённой функции
function functionName<T>(parameter: T): T {
  return parameter;
}

// Разбор по частям:
// 1. function — ключевое слово функции
// 2. functionName — имя функции
// 3. <T> — параметр типа (объявление generics)
// 4. (parameter: T) — параметр функции с типом T
// 5. : T — возвращаемый тип

Параметр типа <T> объявляется перед круглыми скобками параметров. Он доступен в теле функции как обычный тип.

Важно:

Параметр типа <T> — это объявление, а не использование. Он говорит TypeScript: «Эта функция принимает неизвестный тип, который будет определён позже».

Вывод типа (Type Inference)

Одна из самых мощных возможностей generics — автоматический вывод типа. TypeScript анализирует аргументы функции и сам определяет, какой конкретный тип подставляется вместо T:

function identity<T>(value: T): T {
  return value;
}

// Вариант 1: TypeScript выводит тип автоматически
const a = identity(42); // T = number
const b = identity("hello"); // T = string
const c = identity({ name: "test" }); // T = { name: string }

// Вариант 2: Явное указание типа
const d = identity<number>(42); // Явно указали number

// TypeScript знает типы:
a.toFixed(2); // OK! a — number
b.toUpperCase(); // OK! b — string

Вв большинстве случаевв случае вывод типа работает отлично, и вам не нужно указывать тип явно. TypeScript достаточно «умён», чтобы определить тип из контекста.

Когда нужен явный вывод типа?

Иногда TypeScript не может вывести тип автоматически. Тогда нужно указывать его явно:

function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

// TypeScript выведет оба типа:
const result = merge({ a: 1 }, { b: "hello" });
// T = { a: number }, U = { b: string }

// Иногда нужно указать тип явно для пустых объектов:
const empty = merge<User, Address>(
  getEmptyUser(),
  getEmptyAddress()
);

Явное указание типа полезно, когда:

  • Функция возвращает Promise и нужен точный тип
  • Вы работаете с пустыми объектами или массивами
  • Тип слишком сложный для автоматического вывода
  • Вы хотите сделать код более явным для читателей

Несколько параметров типа

Обобщённые функции могут принимать несколько параметров типа. Это полезно, когда функция работает с несколькими разными типами данных одновременно:

// Два параметра типа: T и U
function makePair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

// TypeScript выводит оба типа:
const pair1 = makePair(1, "hello"); // [number, string]
const pair2 = makePair("age", 25); // [string, number]
const pair3 = makePair(true, [1, 2]); // [boolean, number[]]

// Доступ к элементам кортежа:
pair1[0].toFixed(2); // OK! pair1[0] — number
pair1[1].toUpperCase(); // OK! pair1[1] — string

Обратите внимание: возвращаемый тип — это [T, U] — кортеж из двух разных типов.

Практический пример: сортировка

Давайте напишем обобщённую функцию сортировки, которая работает с любым типом данных:

// Обобщённая функция сортировки
function sortBy<T>(
  arr: T[],
  key: (item: T) => number | string
): T[] {
  return [...arr].sort((a, b) => {
    const keyA = key(a);
    const keyB = key(b);
    if (keyA < keyB) return -1;
    if (keyA > keyB) return 1;
    return 0;
  });
}

// Использование с числами:
const nums = [3, 1, 4, 1, 5, 9];
sortBy(nums, n => n); // [1, 1, 3, 4, 5, 9]

// Использование с объектами:
const users = [
  { name: "Мария", age: 30 },
  { name: "Алексей", age: 25 },
  { name: "Борис", age: 35 }
];

sortBy(users, u => u.age); // Сортировка по возрасту
sortBy(users, u => u.name); // Сортировка по имени

Обратите внимание на параметр key: это функция, которая извлекает ключ сортировки из элемента. Она тоже типобезопасна — принимает элемент типа T.

Практический пример: группировка

Ещё один полезный пример — функция группировки элементов по ключу:

// Группировка по ключу
function groupBy<T, K extends string | number>(
  arr: T[],
  keyFn: (item: T) => K
): Record<K, T[]> {
  const result = {} as Record<K, T[]>;
  for (const item of arr) {
    const key = keyFn(item);
    if (!result[key]) {
      result[key] = [];
    }
    result[key].push(item);
  }
  return result;
}

// Группировка пользователей по роли:
const users = [
  { name: "Алиса", role: "admin" },
  { name: "Борис", role: "user" },
  { name: "Виктор", role: "admin" },
  { name: "Галина", role: "user" }
];

const grouped = groupBy(users, u => u.role);
// {
// admin: [{ name: "Алиса", role: "admin" }, { name: "Виктор", role: "admin" }],
// user: [{ name: "Борис", role: "user" }, { name: "Галина", role: "user" }]
// }

Обобщённые стрелочные функции

Обобщённые функции также могут быть записаны в виде стрелочных функций. Синтаксис немного отличается:

// Обычная функция
function identity<T>(value: T): T {
  return value;
}

// Стрелочная функция — параметр типа идёт ПЕРЕД скобками параметров
const identity2 = <T>(value: T): T => value;

// Использование одинаковое:
identity(42); // number
identity2(42); // number
Важное отличие:

В стрелочной функции параметр типа <T> ставится перед скобками параметров, а не после имени функции (как в обычных функциях).

Ограничение типа в параметрах функции

Иногда нужно ограничить тип T, чтобы он не был абсолютно любым. Это делается с помощью ключевого слова extends:

// T должен быть объектом (иметь свойство length)
function logLength<T extends { length: number }>(value: T): T {
  console.log(`Длина: ${value.length}`);
  return value;
}

// Работает с типами, у которых есть length:
logLength("hello"); // OK! string имеет length
logLength([1, 2, 3]); // OK! массив имеет length
logLength({ length: 10 }); // OK!

// Ошибка: number не имеет length
// logLength(42); // Ошибка компиляции!

Мы подробно разберём extends в следующем уроке, но важно понимать базовый принцип: вы можете ограничить T, чтобы он соответствовал определённой структуре.

Обобщённые типы возвращаемого значения

Обобщённые функции могут возвращать сложные типы, основанные на параметрах типа:

// Возвращаем объект с оригиналом и результатом
function wrap<T>(value: T): { original: T; wrapped: string } {
  return {
    original: value,
    wrapped: `[ ${value} ]`
  };
}

const result = wrap(42);
result.original.toFixed(2); // OK! number
result.wrapped.toUpperCase(); // OK! string

Практический пример: дебаунс

Функция debounce — это классический пример обобщённой функции, которая работает с любой функцией:

// Обобщённая функция debounce
function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): T {
  let timeoutId: ReturnType<typeof setTimeout> | null = null;

  return ((...args: any[]) => {
    if (timeoutId) clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      fn(...args);
    }, delay);
  }) as T;
}

// Использование:
function handleSearch(query: string) {
  console.log(`Поиск: ${query}`);
}

// Дебаунс: обработчик вызывается через 300мс после последнего нажатия
const debouncedSearch = debounce(handleSearch, 300);
debouncedSearch("hello"); // Вызовется через 300мс

Практический пример: безопасное получение значения

Обобщённая функция, которая безопасно извлекает значение из объекта по ключу:

// Безопасное извлечение значения по ключу
function getProperty<T, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

const user = { name: "Алексей", age: 25, email: "a@b.com" };

getProperty(user, "name"); // string — TypeScript знает тип!
getProperty(user, "age"); // number
// getProperty(user, "phone"); // Ошибка: "phone" нет в типе user

Здесь мы использовали keyof T — это специальный тип, который возвращаетобъединение тип всех ключей объекта T. Мы подробно разберём его в уроке 6_4.

Советы по использованию обобщённых функций

Делайте:
  • Позволяйте TypeScript выводить тип автоматически
  • Используйте осмысленные имена для параметров типа
  • Ограничивайте тип, если нужна определённая структура
  • Пишите дженерики для повторяющихся паттернов
Не делайте:
  • Не указывайте тип явно без необходимости
  • Не создавайте generics для простых случаев
  • Не используйте any вместо generics
  • Не усложняйте типы без причины

Итоги урока

  • Синтаксис обобщённой функции: function name<T>(param: T): T
  • TypeScript автоматически выводит тип T на основе аргументов
  • Можно указать тип явно: func<number>(42)
  • Несколько параметров типа: func<T, U>(a: T, b: U)
  • В стрелочных функциях <T> ставится перед скобками параметров
  • Обобщённые функции сохраняют типобезопасность
  • Используйте generics для повторяющихся паттернов с разными типами

Практический пример: типобезопасный fetch

Обобщённые функции идеально подходят для типизации HTTP-запросов:

// Типизированная функция fetch
async function fetchJson<T>(
  url: string,
  options?: RequestInit
): Promise<T> {
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

// Использование с конкретным типом:
interface User { id: number; name: string; }

// TypeScript знает, что результат — User
const user = await fetchJson<User>("/api/user/1");
console.log(user.name); // OK! TypeScript знает тип

// Использование с массивом:
const users = await fetchJson<User[]>("/api/users");
users.forEach(u => console.log(u.name));

Практический пример: типобезопасный localStorage

Обобщённые функции для работы с localStorage с типизацией:

// Типизированное хранилище
function getStorageItem<T>(
  key: string,
  defaultValue: T
): T {
  const item = localStorage.getItem(key);
  if (item === null) return defaultValue;
  try {
    return JSON.parse(item) as T;
  } catch {
    return defaultValue;
  }
}

function setStorageItem<T>(
  key: string,
  value: T
): void {
  localStorage.setItem(key, JSON.stringify(value));
}

// Использование:
const theme = getStorageItem("theme", "light"); // string
setStorageItem("theme", "dark");

Следующий урок

Обобщённые интерфейсы и классы — разберём, как создавать обобщённые структуры данных

Тест по обобщённым функциям

8 вопросов

Пара ключ-значение

Premium