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