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

Зачем нужны generics

Проблема дублирования кода и решение через обобщённые типы

Цели урока

  • Понять, какую проблему решают generics
  • Увидеть дублирование кода без generics
  • Осознать аналогию шаблона/формы
  • Увидеть, как generics устраняют дублирование

Проблема, с которой сталкивается каждый разработчик

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

// Функция для получения первого элемента числового массива
function getFirstNumber(arr: number[]): number {
  return arr[0];
}

// Функция для получения первого элемента строкового массива
function getFirstString(arr: string[]): string {
  return arr[0];
}

// Функция для получения первого элемента булевого массива
function getFirstBoolean(arr: boolean[]): boolean {
  return arr[0];
}

Заметьте, что все три функции делают абсолютно одно и то же — берут первый элемент массива и возвращают его. Единственное различие — это тип данных: number, string или boolean.

Задумайтесь:

А если завтра понадобится функция для массива объектов User? Или для массива дат Date? Придётся писать ещё и ещё функции, каждая из которых будет отличаться только типом.

Масштаб проблемы

Давайте посмотрим, как выглядит код, когда мы пытаемся решить проблему без generics. Возьмём более сложный пример — функцию для поиска элемента в массиве:

// Поиск числа в числовом массиве
function findNumber(arr: number[], value: number): number | undefined {
  return arr.find(item => item === value);
}

// Поиск строки в строковом массиве
function findString(arr: string[], value: string): string | undefined {
  return arr.find(item => item === value);
}

// Поиск объекта в массиве объектов
interface User { id: number; name: string; }
function findUser(arr: User[], value: User): User | undefined {
  return arr.find(item => item.id === value.id);
}

Каждая функция — это копия предыдущей с заменой типа. Это нарушает принцип DRY (Don't Repeat Yourself). Мы пишем один и тот же код снова и снова, меняя только тип данных.

Почему это плохо:
  • Если нужно исправить логику — придётся менять в каждой функции
  • Код разбухает и становится трудноподдерживаемым
  • Легко забыть обновить одну из копий
  • Читатель кода не может сразу понять паттерн

Аналогия из реальной жизни: шаблон и форма

Представьте, что вы работаете на заводе, который производит пластиковые стулья. У вас есть металлическая форма (литьевая форма), в которую заливают расплавленный пластик. Эта форма определяет структуру стула: высота спинки, ширина сиденья, форма ножек.

Но пластик может быть разного цвета: красный, синий, зелёный. Форма одна и та же, а результат разный — зависит от того, какой пластик вы залили.

Как это связано с generics:

Форма — это как обобщённая функция (generic function). Она описывает структуру (логику), но не знает, с каким типом данных будет работать. Цвет пластика — это как конкретный тип (number, string, boolean и т.д.), который вы подставляете при использовании.

Ещё одна аналогия — это чертёж здания. Архитектор рисует чертёж, на котором указаны размеры комнат, расположение стен, но не указано, из какого материала будут стены (кирпич, дерево, бетон). Чертёж — это шаблон, а материал — конкретный тип данных.

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

Решение: обобщённые типы (generics)

Generics позволяют создать одну функцию, которая работает с любым типом, сохраняя при этом типобезопасность. Синтаксис — угловые скобки с параметром типа:

// Обобщённая функция — одна на все типы!
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

// Теперь одна функция работает для любых типов:
const num = getFirst([1, 2, 3]); // number
const str = getFirst(["a", "b", "c"]); // string
const bool = getFirst([true, false]); // boolean

Посмотрите на это волшебство: <T> — это параметр типа. Буква T — это условное обозначение, как переменная для типа. Она может быть любой буквой или словом (обычно используют T, U, K, V и т.д.).

Как работает вывод типа (type inference)

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

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

// TypeScript автоматически выводит тип:
const result1 = identity(42); // T = number
const result2 = identity("hello"); // T = string
const result3 = identity(true); // T = boolean

// Можно указать тип явно:
const result4 = identity<number>(42); // Явно указали number

// TypeScript проверяет типы:
result1.toFixed(2); // OK! number имеет toFixed
result2.toUpperCase(); // OK! string имеет toUpperCase
// result3.toUpperCase(); // Ошибка! boolean не имеет toUpperCase

Обратите внимание: после вызова identity(42) TypeScript «запомнил», что T — это number, и позволяет вызывать методы чисел. А для строки — методы строк. Это и есть сила generics: типобезопасность сохраняется.

Сравнение: с generics и без

Давайте прямо сравним два подхода, чтобы увидеть разницу:

Без generics (плохо):
function firstNum(a: number[]): number {
  return a[0];
}
function firstStr(a: string[]): string {
  return a[0];
}
function firstBool(a: boolean[]): boolean {
  return a[0];
}
С generics (хорошо):
function first<T>(a: T[]): T {
  return a[0];
}
first([1, 2, 3])      // number
first(["a", "b"])     // string
first([true, false])  // boolean

Видите? С generics — одна функция вместо трёх. При этом TypeScript всё ещё знает точные типы: first([1, 2, 3]) возвращает number, а first(["a", "b"]) — string.

Generics для интерфейсов

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

// Обобщённый интерфейс для ответа API
interface ApiResponse<T> {
  status: "success" | "error";
  data: T;
  message?: string;
}

// Использование с разными типами данных:
const userResponse: ApiResponse<User> = {
  status: "success",
  data: { name: "Алексей", age: 25 }
};

const listResponse: ApiResponse<string[]> = {
  status: "success",
  data: ["яблоко", "банан", "апельсин"]
};

Один интерфейс ApiResponse описывает структуру ответа для любых данных. Структура одна, а данные могут быть какими угодно.

Generics для классов

Классы тоже могут быть обобщёнными. Это позволяет создавать контейнеры для данных любого типа:

// Обобщённый класс-контейнер
class Box<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

// Использование:
const numBox = new Box<number>(42);
const strBox = new Box<string>("привет");

numBox.getValue(); // 42 (number)
strBox.getValue(); // "привет" (string)

Почему буква T?

Вы, наверное, заметили, что в большинстве примеров используется буква T. Это не случайность — это традиция, пришедшая из языков программирования (в первую очередь из C++ и Java).

Общепринятые обозначения:

T Type (тип) — основной параметр типа
U, V Второй, третий параметр типа
K Key (ключ) — для ключей объектов
V Value (значение) — для значений объектов
Совет:

Можно использовать любые имена: <Item>, <ElementType>, <Data>. Но лучше придерживаться стандартных обозначений — это облегчит чтение кода другими разработчиками.

Практический пример: функция для слияния объектов

Давайте рассмотрим более практичный пример. Функция, которая объединяет два объекта:

// Без generics — потеря типизации
function merge(obj1: any, obj2: any): any {
  return { ...obj1, ...obj2 };
}

// С generics — типы сохраняются!
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

// TypeScript знает типы свойств!
const result = merge({ name: "Алексей" }, { age: 25 });
result.name; // string — TypeScript знает!
result.age; // number — TypeScript знает!

С двумя параметрами типа <T, U> мы можем объединять объекты разных типов и при этом сохранять информацию о типах каждого свойства.

Обобщённые массивы

Вы уже встречали обобщённые типы для массивов — Array<T>. Это то же самое, что T[]:

// Оба варианта эквивалентны:
const nums1: number[] = [1, 2, 3];
const nums2: Array<number> = [1, 2, 3];

// Array<T> — это обобщённый тип из стандартной библиотеки TypeScript
// Он определён примерно так:
// interface Array<T> {
// length: number;
// push(item: T): number;
// pop(): T | undefined;
// // ...
// }

Когда использовать generics?

Generics — мощный инструмент, но не нужно использовать их везде. Вот когда они действительно нужны:

Когда использовать:
  • Функция работает с разными типами, но логика одинаковая
  • Контейнеры данных (списки, очереди, стеки)
  • Обобщённые интерфейсы для API
  • Утилиты и хелпер-функции
Когда НЕ использовать:
  • Функция работает только с одним конкретным типом
  • Слишком сложная логика, затрудняющая чтение
  • Простые геттеры/сеттеры
  • Когда any подходит лучше (редкие случаи)

Практический пример: функция фильтрации

Давайте напишем функцию фильтрации, которая принимает массив и предикат (условие фильтрации) и возвращает отфильтрованный массив:

// Обобщённая функция фильтрации
function filterArray<T>(
  arr: T[],
  predicate: (item: T) => boolean
): T[] {
  const result: T[] = [];
  for (const item of arr) {
    if (predicate(item)) {
      result.push(item);
    }
  }
  return result;
}

// Использование с числами:
const evenNumbers = filterArray(
  [1, 2, 3, 4, 5, 6],
  num => num % 2 === 0
); // [2, 4, 6]

// Использование со строками:
const longStrings = filterArray(
  ["привет", "мир", "длинная строка"],
  str => str.length > 5
); // ["привет", "длинная строка"]

Обратите внимание: функция filterArray работает с любым типом данных, а предикат predicate тоже типобезопасен — он принимает элемент того же типа T.

Generics в стандартной библиотеке TypeScript

Generics повсюду в стандартной библиотеке TypeScript. Вы уже использовали их, даже не замечая:

// Array — обобщённый тип
const nums: Array<number> = [1, 2, 3];

// Promise — обобщённый тип
const promise: Promise<string> = fetch("/api").then(r => r.text());

// Map — обобщённый тип
const map = new Map<string, number>();
map.set("age", 25);

// Set — обобщённый тип
const set = new Set<string>();
set.add("hello");

// Record — обобщённый тип
const scores: Record<string, number> = {
  math: 95,
  english: 88
};

Как видите, generics — это не просто теория. Они используются повсюду в реальном коде. Понимание generics важно для чтения и написания типизированного TypeScript-кода.

Итоги урока

  • Generics — это обобщённые типы, которые позволяют создавать функции, классы и интерфейсы, работающие с любыми типами данных
  • Без generics приходится писать дублирующий код для каждого типа
  • Синтаксис: function name<T>(param: T): T
  • TypeScript автоматически выводит тип на основе переданных аргументов
  • Generics сохраняют типобезопасность — вы всё ещё получаете автодополнение и проверку типов
  • Аналогия: шаблон/форма, которая работает с любым «материалом» (типом данных)
  • Generics используются повсюду: Array, Promise, Map, Set и другие стандартные типы

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

Обобщённые функции — подробно разберём синтаксис <T>, вывод типа и работу с несколькими параметрами типа

Тест по generics

8 вопросов

Первый элемент массива

Premium