Зачем нужны 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). Мы пишем один и тот же код снова и снова, меняя только тип данных.
- Если нужно исправить логику — придётся менять в каждой функции
- Код разбухает и становится трудноподдерживаемым
- Легко забыть обновить одну из копий
- Читатель кода не может сразу понять паттерн
Аналогия из реальной жизни: шаблон и форма
Представьте, что вы работаете на заводе, который производит пластиковые стулья. У вас есть металлическая форма (литьевая форма), в которую заливают расплавленный пластик. Эта форма определяет структуру стула: высота спинки, ширина сиденья, форма ножек.
Но пластик может быть разного цвета: красный, синий, зелёный. Форма одна и та же, а результат разный — зависит от того, какой пластик вы залили.
Форма — это как обобщённая функция (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 сам определяет, какой тип вы используете, на основе переданных аргументов:
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 и без
Давайте прямо сравним два подхода, чтобы увидеть разницу:
function firstNum(a: number[]): number { return a[0]; } function firstStr(a: string[]): string { return a[0]; } function firstBool(a: boolean[]): boolean { return a[0]; }
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 работают не только с функциями. Вы можете создавать обобщённые интерфейсы, которые описывают структуру данных, не привязанную к конкретному типу:
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>. Но лучше придерживаться стандартных обозначений — это облегчит чтение кода другими разработчиками.
Практический пример: функция для слияния объектов
Давайте рассмотрим более практичный пример. Функция, которая объединяет два объекта:
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. Вы уже использовали их, даже не замечая:
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 вопросов