TypeScript - Generic Classes

Здравствуйте, будущие супергерои кодирования! Сегодня мы погружаемся в захватывающий мир генерических классов TypeScript. Не волнуйтесь, если вы новички в программировании; я буду вести вас по этому пути шаг за шагом, как я уже делал для countless студентов на протяжении многих лет моего преподавания. Так что возьмите любимый напиток, устройтесь поудобнее и отправляйтесь в это приключение вместе со мной!

TypeScript - Generic Classes

Generic Classes

Что такое генерические классы?

Представьте, что вы находитесь вмагазине мороженого, но вместо выбора вкусов вы выбираете типы данных. Вот суть генерических классов! Они позволяют нам создавать гибкие, повторно используемые компоненты, которые могут работать с различными типами данных, не жертвуя типобезопасностью.

Давайте начнем с простого примера:

class Box<T> {
private content: T;

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

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

В этом примере Box - это генерический класс. <T> - это как placeholder для типа, который мы укажем позже. Это как сказать в магазине мороженого: "Я решу, какой вкус взять, когда закажу!"

Давайте разберем это:

  • class Box<T>: Это declares a generic class named Box с типовым параметром T.
  • private content: T: Мы говорим, что content будет типа T,ichever T окажется.
  • constructor(value: T): Конструктор принимает значение типа T.
  • getValue(): T: Этот метод возвращает значение типа T.

Теперь давайте посмотрим, как мы можем использовать этот класс:

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

let stringBox = new Box<string>("Привет, TypeScript!");
console.log(stringBox.getValue()); // Вывод: Привет, TypeScript!

Не правда ли, здорово? Мы использовали один и тот же класс Box для хранения и числа, и строки. Это как магическая коробка, которая может holding anything вы положите в нее, но все равно помнит exactly what type of thing она содержит!

Множественные типовые параметры

Иногда одного типового параметра недостаточно. Давайте создадим более сложный пример с множественными типовыми параметрами:

class Pair<T, U> {
private first: T;
private second: U;

constructor(first: T, second: U) {
this.first = first;
this.second = second;
}

getFirst(): T {
return this.first;
}

getSecond(): U {
return this.second;
}
}

Этот класс Pair может содержать два значения потенциально разных типов. Это как двойной рожок мороженого, где каждая порция может быть другого вкуса!

Давайте используем наш класс Pair:

let pair = new Pair<string, number>("Возраст", 30);
console.log(pair.getFirst());  // Вывод: Возраст
console.log(pair.getSecond()); // Вывод: 30

Ограничения генериков

Иногда мы хотим ограничить, какие типы могут использоваться с是我们的ими генерическими классами. Мы можем сделать это с помощью ограничений. Это как сказать: "Вы можете взять любое мороженое, только не слишком острое!"

interface Lengthwise {
length: number;
}

class LengthChecker<T extends Lengthwise> {
checkLength(obj: T): string {
return `Длина: ${obj.length}`;
}
}

В этом примере T extends Lengthwise означает, что T должен быть типом, который имеет свойство length. Давайте используем его:

let stringChecker = new LengthChecker<string>();
console.log(stringChecker.checkLength("Привет")); // Вывод: Длина: 6

let arrayChecker = new LengthChecker<number[]>();
console.log(arrayChecker.checkLength([1, 2, 3])); // Вывод: Длина: 3

// Это вызовет ошибку:
// let numberChecker = new LengthChecker<number>();
// Type 'number' does not satisfy the constraint 'Lengthwise'.

Реализация генерического интерфейса с генерическим классом

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

Сначала давайте определим генерический интерфейс:

interface Repository<T> {
getById(id: number): T;
save(item: T): void;
}

Этот интерфейс Repository определяет контракт для классов, которые будут обрабатывать хранение и retrieval данных. Теперь давайте реализуем этот интерфейс с помощью генерического класса:

class GenericRepository<T> implements Repository<T> {
private items: T[] = [];

getById(id: number): T {
return this.items[id];
}

save(item: T): void {
this.items.push(item);
}
}

Наш класс GenericRepository реализует интерфейс Repository. Он может работать с любым типом T. Давайте используем его:

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

let userRepo = new GenericRepository<User>();

userRepo.save({ name: "Алиса", age: 30 });
userRepo.save({ name: "Боб", age: 25 });

console.log(userRepo.getById(0)); // Вывод: { name: "Алиса", age: 30 }
console.log(userRepo.getById(1)); // Вывод: { name: "Боб", age: 25 }

В этом примере мы создали репозиторий для объектов User. Но красота нашей генерической реализации заключается в том, что мы могли бы так же легко создать репозиторий для любого другого типа!

Методы таблицы

Вот удобная таблица, резюмирующая методы, которые мы рассмотрели:

Метод Описание Пример
constructor(value: T) Создает новый экземпляр генерического класса new Box<number>(42)
getValue(): T Возвращает значение, хранящееся в генерическом классе numberBox.getValue()
getFirst(): T Возвращает первое значение в паре pair.getFirst()
getSecond(): U Возвращает второе значение в паре pair.getSecond()
checkLength(obj: T): string Проверяет длину объекта (с ограничениями) stringChecker.checkLength("Привет")
getById(id: number): T Получает элемент из репозитория по ID userRepo.getById(0)
save(item: T): void Сохраняет элемент в репозиторий userRepo.save({ name: "Алиса", age: 30 })

И вот оно, folks! Мы прошли путь через страну генерических классов TypeScript, от простых коробок до сложных репозиториев. Помните, что практика makes perfect, так что не бойтесь экспериментировать с этими концепциями. Кто знает? Вы можете создать следующее большое дело в программировании! До свидания, счастливого кодирования!

Credits: Image by storyset