Как работают дженерики в 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. Понимание и освоение дженериков может значительно повысить способность писать эффективный и безошибочный код.