Как работают дженерики в TypeScript

Источник: «How TypeScript Generics Work – Explained with Examples»
TypeScript с его мощной системой типов предлагает функцию Дженерики, позволяющую разработчикам писать многократно используемый и типобезопасный код. Дженерики позволяют создавать компоненты, способные работать с различными типами, а не с одним.

Эта статья посвящена дженерикам в TypeScript и содержит объяснения и примеры кода, иллюстрирующие их использование и преимущества.

Вы можете скачать весь исходный код с GitHub.

Что такое дженерики

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

Дженерики представлены параметрами типа, выступающими в роли держателей типов. Эти параметры указываются в угловых скобках (<>) и могут использоваться во всем коде для определения типов переменных, параметров функций, возвращаемых типов и т. д.

Примеры использования дженериков в TypeScript

Базовое использование дженериков

Начнём с простого примера функции дженерика:

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

let output = identity<string>("hello");
console.log(output); // Вывод: hello

В этом примере identity — функция дженерик, принимающая параметр типа T. Параметр arg имеет тип T, и возвращаемый тип функции также T. При вызове identity<string>("hello") параметр типа T воспринимается как string, что обеспечивает безопасность типов.

Как использовать классы дженерики

Дженерики не ограничиваются функциями — их можно использовать и с классами. Рассмотрим следующий пример класса дженерика Box:

class Box<T> {
private value: T;

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

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

let box = new Box<number>(42);
console.log(box.getValue()); // Вывод: 42

В данном случае Box — класс дженерик с параметром типа T. Конструктор принимает значение типа T, а метод getValue возвращает значение типа T. При создании экземпляра Box<number> он может хранить и возвращать только значения типа number.

Как применять ограничения к дженерикам

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

interface Lengthwise {
length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

let result = loggingIdentity("hello");
console.log(result);

// Вывод: 5
// hello

В этом примере функция loggingIdentity принимает параметр типа T, расширяющий интерфейс Lengthwise, гарантирующий, что arg имеет свойство length. Это ограничение позволяет получить доступ к свойству length, не вызывая ошибки компиляции.

Как использовать дженерики с интерфейсами

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

interface Pair<T, U> {
first: T;
second: U;
}

let pair: Pair<number, string> = { first: 1, second: "two" };
console.log(pair); // Вывод: { first: 1, second: "two" }

В данном случае Pair — интерфейс с двумя параметрами типа T и U, представляющими типы свойств first и second. При объявлении pair как Pair<number, string> выполняется условие, что свойство first должно быть числом, а свойство second должно быть строкой.

Как использовать функции дженерики с массивом

function reverse<T>(array: T[]): T[] {
return array.reverse();
}

let numbers: number[] = [1, 2, 3, 4, 5];
let reversedNumbers: number[] = reverse(numbers);
console.log(reversedNumbers); // Вывод: [5, 4, 3, 2, 1]

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

Как использовать ограничения дженерика с keyof

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

let person = { name: "John", age: 30, city: "New York" };
let age: number = getProperty(person, "age");
console.log(age); // Вывод: 30

Здесь функция getProperty принимает объект типа T и ключ типа K, где K расширяет ключи T. Затем она возвращает соответствующее значение свойства объекта. Этот пример демонстрирует, как использовать дженерики с keyof для обеспечения безопасности типов при доступе к свойствам объектов.

Как использовать утилитарные функции дженерики

function toArray<T>(value: T): T[] {
return [value];
}

let numberArray: number[] = toArray(42);
console.log(numberArray); // Вывод: [42]

let stringArray: string[] = toArray("hello");
console.log(stringArray); // Вывод: ["hello"]

Функция toArray преобразует одно значение типа T в массив, содержащий это значение. Эта простая утилитарная функция демонстрирует, как можно использовать дженерики для создания многократно используемого кода, адаптирующегося к различным типам данных без особых усилий.

Как использовать интерфейсы дженерики в функции

interface Transformer<T, U> {
(input: T): U;
}

function uppercase(input: string): string {
return input.toUpperCase();
}

let transform: Transformer<string, string> = uppercase;
console.log(transform("hello")); // Вывод: HELLO

В этом примере мы определяем интерфейс Transformer с двумя параметрами типа T и U, представляющими входящий и выходящий типы соответственно. Далее объявляем функцию uppercase и присваиваем её переменной transform типа Transformer<string, string>. Это демонстрирует, как дженерики могут использоваться для определения гибких интерфейсов для функций.

Заключение

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

Дополнительные материалы

Предыдущая Статья

Хватит использовать устаревшие методы оптимизации в PHP

Следующая Статья

Пять новых возможностей JavaScript в 2024 году