Введение в Generics TypeScript
TypeScript — это строго типизированный язык программирования, который добавляет статическую типизацию в JavaScript. Одной из самых мощных и часто недооцененных возможностей TypeScript являются Generics (дженерики, обобщенные типы). Если вы когда-либо писали функцию, которая должна работать с разными типами данных, или создавали компонент, который должен быть универсальным, то Generics — это именно то, что вам нужно.
В этой статье мы подробно разберем, что такое дженерики в TypeScript, зачем они нужны и как их использовать на практике. Вы узнаете, как сделать ваш код более гибким, безопасным и переиспользуемым.
Что такое Generics и зачем они нужны?
Generics (обобщенные типы) — это механизм, который позволяет создавать компоненты (функции, классы, интерфейсы), работающие с различными типами данных, не теряя при этом информацию о типе. Представьте, что вы пишете функцию, которая возвращает первый элемент массива. Без дженериков вам пришлось бы использовать тип any, что лишает вас преимуществ строгой типизации.
Зачем использовать Generics?
- Безопасность типов: TypeScript проверяет типы на этапе компиляции, предотвращая ошибки.
- Переиспользование кода: Одна функция или класс могут работать с любыми типами данных.
- Улучшение читаемости: Код становится более понятным и самодокументируемым.
- Сокращение дублирования: Вам не нужно писать отдельные функции для каждого типа данных.
Без Generics вы бы написали что-то вроде этого:
// Плохо: теряем информацию о типе
function getFirstElement(arr: any[]): any {
return arr[0];
}
const num = getFirstElement([1, 2, 3]); // Тип any
console.log(num.toUpperCase()); // Ошибка во время выполнения!
С Generics код становится безопасным и гибким:
// Хорошо: сохраняем тип
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = getFirstElement([1, 2, 3]); // Тип number
const str = getFirstElement(['a', 'b', 'c']); // Тип string
// num.toUpperCase(); // Ошибка компиляции! number не имеет метода toUpperCase
Основы синтаксиса Generics
Синтаксис с угловыми скобками
Generics используют угловые скобки <> для указания параметров типа. Обычно параметры обозначаются одной буквой: T (Type), U, V и т.д. Однако вы можете использовать любые осмысленные имена.
// Простейший пример: функция с одним параметром типа
function identity<T>(arg: T): T {
return arg;
}
// Явное указание типа
let result1 = identity<string>('Hello');
// Вывод типа (TypeScript сам определяет тип)
let result2 = identity(42); // Тип number
Несколько параметров типа
Вы можете использовать несколько параметров типа в одной функции или классе:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: 'John' }, { age: 30 });
// Тип: { name: string } & { age: number }
console.log(merged.name); // John
console.log(merged.age); // 30
Ограничения (Constraints)
Иногда нужно ограничить типы, которые могут быть переданы в дженерик. Для этого используется ключевое слово extends:
interface HasLength {
length: number;
}
// Функция работает только с типами, у которых есть свойство length
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength('Hello'); // Работает: 5
logLength([1, 2, 3]); // Работает: 3
// logLength(123); // Ошибка! number не имеет length
Применение Generics в функциях, интерфейсах и классах
Generics в функциях
Самое частое применение дженериков — это функции, которые работают с разными типами данных:
// Функция для работы с массивами
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: 'Alice', age: 25, city: 'Moscow' };
const name = getProperty(person, 'name'); // Тип string
const age = getProperty(person, 'age'); // Тип number
// getProperty(person, 'salary'); // Ошибка! 'salary' не является ключом объекта
Generics в интерфейсах
Интерфейсы с дженериками позволяют создавать гибкие структуры данных:
// Обобщенный интерфейс для ответа API
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
// Использование с разными типами
interface User {
id: number;
name: string;
}
const userResponse: ApiResponse<User> = {
success: true,
data: { id: 1, name: 'John' }
};
const listResponse: ApiResponse<string[]> = {
success: true,
data: ['apple', 'banana', 'cherry']
};
Generics в классах
Классы с дженериками позволяют создавать переиспользуемые компоненты:
// Простой стек с дженериками
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
// Использование
const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20
const stringStack = new Stack<string>();
stringStack.push('Hello');
console.log(stringStack.peek()); // Hello
Расширенные возможности Generics
Условные типы (Conditional Types)
TypeScript позволяет создавать типы, которые зависят от условий:
type IsString<T> = T extends string ? 'yes' : 'no';
type Test1