Основы TypeScript: Дженерики (Generics)
Дженерики (обобщенные типы) в TypeScript предоставляют способ параметризовать типы. Подобно тому, как функции принимают параметры, дженерики позволяют классам, интерфейсам и функциям принимать типы в качестве параметров. Это позволяет создавать компоненты, которые могут работать с различными типами данных, обеспечивая при этом типовую безопасность и предотвращая ошибки времени выполнения, связанные с неправильным использованием типов.
Зачем нужны дженерики?
Рассмотрим пример без использования дженериков:
function identity(arg: any): any {
return arg;
}
let outputNumber = identity(123); // outputNumber имеет тип any
let outputString = identity("hello"); // outputString имеет тип anyФункция identity принимает аргумент типа any и возвращает значение типа any. Хотя эта функция работает, она теряет информацию о типе аргумента. Мы не знаем, какой тип был передан, и какой тип будет возвращен. Дженерики позволяют нам захватить тип, который был передан, и использовать эту информацию позже.
Синтаксис дженериков
В TypeScript дженерики обычно обозначаются с помощью угловых скобок <T>, где T — это имя параметра типа (можно использовать любое допустимое имя, но T (Type), K (Key), V (Value), E (Element) часто используются по соглашению).
Дженерики в функциях
function identity<T>(arg: T): T {
return arg;
}
let outputNumber = identity<number>(123); // T теперь number, outputNumber имеет тип number
let outputString = identity<string>("hello"); // T теперь string, outputString имеет тип string
let outputBoolean = identity<boolean>(true); // T теперь boolean, outputBoolean имеет тип boolean
// TypeScript также может выводить тип аргумента автоматически
let inferredNumber = identity(456); // inferredNumber имеет тип number
let inferredString = identity("world"); // inferredString имеет тип stringВ этом примере <T> после имени функции identity делает T параметром типа. Когда мы вызываем функцию, мы можем явно указать тип аргумента (например, identity<number>(123)), или TypeScript может вывести его автоматически. Возвращаемый тип функции также является T, что гарантирует, что возвращается значение того же типа, что и был передан аргумент.
Дженерики в интерфейсах
Дженерики также можно использовать в интерфейсах для создания обобщенных структур.
interface Box<T> {
contents: T;
}
let numberBox: Box<number> = { contents: 100 };
let stringBox: Box<string> = { contents: "TypeScript" };
function getContents<T>(box: Box<T>): T {
return box.contents;
}
let numberContent = getContents(numberBox); // numberContent имеет тип number
let stringContent = getContents(stringBox); // stringContent имеет тип stringЗдесь интерфейс Box является дженериком, который принимает параметр типа T. Свойство contents внутри Box имеет тип T. При создании экземпляров Box, мы указываем конкретный тип для T (например, Box<number> или Box<string>).
Дженерики в классах
Классы также могут быть дженериками. Параметр типа указывается после имени класса.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, add: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = add;
}
}
let numberCalculator = new GenericNumber<number>(0, (a, b) => a + b);
console.log(numberCalculator.add(10, 20)); // Выведет 30
let stringCalculator = new GenericNumber<string>("", (a, b) => a + b);
console.log(stringCalculator.add("hello ", "world")); // Выведет "hello world"Класс GenericNumber параметризован типом T. Свойство zeroValue и тип параметров и возвращаемого значения метода add зависят от T.
Ограничения дженериков (Generic Constraints)
Иногда вам может потребоваться ограничить типы, которые могут быть использованы с вашим дженериком. Например, вы можете захотеть написать функцию, которая работает с объектами, имеющими определенное свойство. Для этого используются ограничения дженериков с помощью ключевого слова extends.
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // OK, string имеет свойство length
logLength([1, 2, 3]); // OK, массив имеет свойство length
// logLength(123); // Ошибка: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.В этом примере дженерик T ограничен типом Lengthwise. Это означает, что T должен быть либо Lengthwise, либо типом, который расширяет Lengthwise (то есть имеет свойство length типа number).
Вы также можете использовать классы в качестве ограничений:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
function createInstance<T extends Animal>(c: new (name: string) => T, name: string): T {
return new c(name);
}
let animal = createInstance(Animal, "Generic Animal");
let dog = createInstance(Dog, "Buddy");Здесь дженерик T ограничен типами, которые являются подклассами Animal. Функция createInstance принимает конструктор типа T и имя, и создает экземпляр этого типа.
Использование параметров типа в ограничениях
Параметр типа может быть ограничен другим параметром типа.
function copyFields<T extends U, U>(target: T, source: U): T {
for (const id in source) {
target[id] = source[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
let y = { b: 10, d: 20 };
copyFields(x, y); // x теперь { a: 1, b: 10, c: 3, d: 20 }Здесь T ограничен типом U. Это гарантирует, что U имеет как минимум те же свойства, что и T.
Дженерики с несколькими параметрами типа
Вы можете использовать несколько параметров типа в дженериках.
interface Pair<K, V> {
key: K;
value: V;
}
let pair1: Pair<string, number> = { key: "id", value: 1 };
let pair2: Pair<number, string> = { key: 101, value: "name" };Преимущества использования дженериков
- Типовая безопасность: Дженерики обеспечивают типовую безопасность на этапе компиляции, что помогает выявлять ошибки до выполнения кода.
- Повторно используемый код: Дженерики позволяют писать код, который может работать с различными типами данных без дублирования логики.
- Лучшая производительность (потенциально): Хотя дженерики существуют только во время компиляции и не влияют на производительность JavaScript во время выполнения, они позволяют компилятору выполнять более строгие проверки типов, что может привести к более оптимизированному коду.
- Более выразительные типы: Дженерики позволяют более точно описывать типы данных, особенно при работе с коллекциями и структурами данных.
Заключение
Дженерики являются мощной и важной частью системы типов TypeScript. Они позволяют создавать гибкий и повторно используемый код, сохраняя при этом преимущества статической типизации. Понимание и эффективное использование дженериков является ключевым для написания качественного и масштабируемого кода на TypeScript.