Generics в TypeScript: полное руководство для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Generics в TypeScript: зачем нужны и как использовать

TypeScript — это мощный инструмент, который добавляет статическую типизацию в JavaScript. Однако, если вы только начинаете изучать TypeScript, вы могли столкнуться с ситуацией, когда приходится писать один и тот же код для разных типов данных. Именно здесь на помощь приходят Generics (обобщенные типы или дженерики). В этой статье мы разберем, что такое Generics, зачем они нужны и как их использовать на практике.

Что такое Generics и почему они важны?

Generics — это механизм TypeScript, который позволяет создавать компоненты (функции, классы, интерфейсы), работающие с различными типами данных, а не с каким-то одним конкретным. Представьте, что вы пишете функцию, которая должна работать и с числами, и со строками, и с массивами. Без дженериков вам пришлось бы либо использовать тип any (что убивает всю суть TypeScript), либо создавать несколько копий одной и той же функции.

Основные преимущества Generics:

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

Базовый синтаксис Generics

Давайте начнем с самого простого примера. Допустим, нам нужна функция, которая возвращает переданный ей аргумент. Без Generics мы бы написали так:

function identity(arg: number): number {
    return arg;
}

Но что, если нам нужно передать строку? Придется создавать новую функцию. С помощью Generics мы делаем так:

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

// Использование:
let output1 = identity<string>("Привет");
let output2 = identity<number>(42);
let output3 = identity<boolean>(true);

Здесь <T> — это параметр типа (или переменная типа). Когда мы вызываем функцию, TypeScript автоматически подставляет нужный тип. Вы можете использовать любую букву: T, U, V или даже осмысленные названия вроде Type.

Кстати, TypeScript может сам вывести тип, поэтому часто можно писать без явного указания:

let output = identity("Привет"); // TypeScript сам поймет, что T = string

Работа с массивами и несколькими типами

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

function getFirstElement<T>(arr: T[]): T | undefined {
    return arr[0];
}

const numbers = [1, 2, 3];
const firstNumber = getFirstElement(numbers); // тип number | undefined

const strings = ["a", "b", "c"];
const firstString = getFirstElement(strings); // тип string | undefined

Обратите внимание: мы использовали T[] — это то же самое, что Array<T>. А что если функция принимает два разных типа? Легко!

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

const result = merge({ name: "Анна" }, { age: 25 });
// result имеет тип { name: string; } & { age: number; }
// Фактически: { name: string; age: number; }

Ограничения Generics (Constraints)

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

Например, мы хотим написать функцию, которая получает длину объекта. Очевидно, что не у всех типов есть свойство length. Давайте это проверим:

function getLength<T extends { length: number }>(item: T): number {
    return item.length;
}

console.log(getLength("Hello")); // 5
console.log(getLength([1, 2, 3])); // 3
// console.log(getLength(123)); // Ошибка! У числа нет length
// console.log(getLength({ name: "Test" })); // Ошибка! Нет свойства length

Это мощный инструмент: вы не просто говорите "работай с любым типом", а накладываете условия. В реальных проектах это часто используется для работы с API, базами данных и сложными структурами.

Другой пример — ограничение по типу ключа объекта:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { id: 1, name: "Иван", role: "admin" };
const userName = getProperty(user, "name"); // тип string
const userId = getProperty(user, "id"); // тип number
// getProperty(user, "email"); // Ошибка! Свойства email нет в объекте

Generic классы и интерфейсы

Дженерики работают не только с функциями, но и с классами и интерфейсами. Это особенно полезно при создании структур данных, таких как стеки, очереди или репозитории.

Пример Generic класса — простой стек:

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];
    }

    get size(): number {
        return this.items.length;
    }
}

// Использование:
const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20

const stringStack = new Stack<string>();
stringStack.push("TypeScript");
console.log(stringStack.peek()); // "TypeScript"

А вот пример Generic интерфейса. Допустим, мы работаем с ответами от API:

interface ApiResponse<T> {
    status: number;
    data: T;
    message: string;
}

interface User {
    id: number;
    name: string;
}

interface Product {
    id: number;
    title: string;
    price: number;
}

// Используем один интерфейс для разных типов данных:
const userResponse: ApiResponse<User> = {
    status: 200,
    data: { id: 1, name: "Анна" },
    message: "Успешно"
};

const productResponse: ApiResponse                

Блоги

Рекомендаций по книгам