Пошук уроків, статей та іншого контенту
Створюємо узагальнені класи зі станом, методами та конструкторами, що працюють із різними типами.
Узагальнений клас — це клас, який працює з типом, переданим під час створення його екземпляра.
Замість того щоб створювати окремий клас для чисел, рядків або об’єктів, можна описати один клас із параметром типу:
class Box<T> {
constructor(public value: T) {}
getValue(): T {
return this.value;
}
}T — це параметр типу. Він не є конкретним типом, а лише позначає тип, який буде визначено пізніше.
Під час створення екземпляра можна вказати тип явно:
const numberBox = new Box<number>(42);
const textBox = new Box<string>("TypeScript");У результаті:
numberBox.value має тип number;
textBox.value має тип string;
метод getValue() повертає відповідний тип.
Параметр типу вказують після назви класу в кутових дужках:
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
setValue(value: T): void {
this.value = value;
}
}У цьому класі всі операції зі станом використовують один і той самий тип T:
властивість value має тип T;
конструктор приймає значення типу T;
getValue() повертає T;
setValue() приймає T.
const container = new Container<number>(10);
container.setValue(25);
console.log(container.getValue()); // 25Передати рядок у такий контейнер не можна:
const container = new Container<number>(10);
// Помилка TypeScript:
// Argument of type 'string' is not assignable to parameter of type 'number'.
container.setValue("25");У більшості випадків TypeScript може визначити параметр типу за аргументом конструктора. Тому тип не обов’язково вказувати вручну:
const numberContainer = new Container(10);
const textContainer = new Container("hello");TypeScript визначить:
// Container<number>
const numberContainer = new Container(10);
// Container<string>
const textContainer = new Container("hello");Явне зазначення типу може бути корисним, коли:
тип не можна надійно вивести з аргументів;
потрібно перевірити, що значення відповідає очікуваному типу;
код має бути зрозумілішим для читача.
const values = new Container<string | number>(42);
values.setValue("forty-two");Тут контейнер може зберігати як string, так і number.
Розглянемо практичніший приклад — узагальнений список. Він зберігає елементи одного типу та надає методи для роботи з ними.
class Collection<T> {
private items: T[];
constructor(initialItems: T[] = []) {
this.items = [...initialItems];
}
add(item: T): void {
this.items.push(item);
}
removeLast(): T | undefined {
return this.items.pop();
}
get(index: number): T | undefined {
return this.items[index];
}
getAll(): T[] {
return [...this.items];
}
get size(): number {
return this.items.length;
}
}
const scores = new Collection<number>([10, 20, 30]);
scores.add(40);
console.log(scores.get(1)); // 20
console.log(scores.size); // 4
console.log(scores.getAll()); // [10, 20, 30, 40]
const lastScore = scores.removeLast();
console.log(lastScore); // 40
const names = new Collection<string>(["Олена", "Марко"]);
names.add("Ірина");
console.log(names.getAll()); // ["Олена", "Марко", "Ірина"]Методи класу автоматично працюють із конкретним типом екземпляра:
scores.add() приймає лише number;
names.add() приймає лише string;
scores.get() повертає number | undefined;
names.get() повертає string | undefined.
Операція get() може повернути undefined, якщо елемента з таким індексом немає. Тому це відображено в типі результату:
const score = scores.get(100);
// score має тип number | undefinedКлас може мати більше одного параметра типу. Наприклад, сховище може пов’язувати ключі зі значеннями:
class KeyValueStore<K, V> {
private entries = new Map<K, V>();
set(key: K, value: V): void {
this.entries.set(key, value);
}
get(key: K): V | undefined {
return this.entries.get(key);
}
has(key: K): boolean {
return this.entries.has(key);
}
}
const userNames = new KeyValueStore<number, string>();
userNames.set(1, "Олена");
userNames.set(2, "Марко");
console.log(userNames.get(1)); // "Олена"
console.log(userNames.has(3)); // falseУ цьому прикладі:
K описує тип ключа;
V описує тип значення;
set() приймає ключ типу K і значення типу V;
get() повертає V | undefined.
TypeScript перевіряє обидва типи:
const products = new KeyValueStore<string, number>();
products.set("keyboard", 2500);
// Помилки типів:
// products.set(1, 2500);
// products.set("mouse", "1500");Параметром типу може бути інтерфейс або тип об’єкта.
interface User {
id: number;
name: string;
}
class Repository<T> {
private records: T[] = [];
add(record: T): void {
this.records.push(record);
}
findAll(): T[] {
return [...this.records];
}
}
const users = new Repository<User>();
users.add({
id: 1,
name: "Олена",
});
users.add({
id: 2,
name: "Марко",
});
console.log(users.findAll());Метод add() перевіряє структуру кожного об’єкта:
users.add({
id: 3,
name: "Ірина",
});
// Помилка: відсутня обов’язкова властивість name
users.add({
id: 4,
});Це дає змогу повторно використовувати Repository<T> для різних моделей:
interface Product {
id: number;
title: string;
price: number;
}
const products = new Repository<Product>();
products.add({
id: 1,
title: "Клавіатура",
price: 2500,
});Іноді клас має працювати не з будь-яким типом, а лише з типами, які мають певні властивості або відповідають певній структурі.
Для цього використовують extends.
interface Identifiable {
id: number;
}
class EntityStore<T extends Identifiable> {
private entities: T[] = [];
add(entity: T): void {
this.entities.push(entity);
}
findById(id: number): T | undefined {
return this.entities.find((entity) => entity.id === id);
}
}
interface User extends Identifiable {
name: string;
}
const userStore = new EntityStore<User>();
userStore.add({
id: 1,
name: "Олена",
});
console.log(userStore.findById(1)); // { id: 1, name: "Олена" }Завдяки обмеженню T extends Identifiable клас може безпечно звертатися до entity.id.
Тип, який передається замість T, повинен мати властивість id типу number:
interface Product extends Identifiable {
title: string;
}
const productStore = new EntityStore<Product>();Тип без необхідної властивості не підходить:
interface Category {
title: string;
}
// Помилка: Category не має властивості id
const categoryStore = new EntityStore<Category>();Обмеження не означає, що тип має бути саме Identifiable. Він може містити додаткові властивості, але зобов’язаний мати мінімально необхідну структуру.
Параметри типу класу та параметри типу методу — різні речі.
Клас може мати власний T, а окремий метод може оголошувати додатковий параметр типу:
class Box<T> {
constructor(private value: T) {}
getValue(): T {
return this.value;
}
map<U>(transform: (value: T) => U): Box<U> {
return new Box(transform(this.value));
}
}
const numberBox = new Box(10);
const textBox = numberBox.map((value) => `Число: ${value}`);
console.log(textBox.getValue()); // "Число: 10"У цьому прикладі:
T — тип значення, яке зберігає клас;
U — тип нового значення, який визначається для конкретного виклику map;
Box<number> перетворюється на Box<string>.
Метод не змінює тип поточного об’єкта, а створює новий об’єкт із результатом перетворення.
Параметр типу класу доступний екземплярним властивостям і методам. Статичні члени належать самому класу, а не конкретному екземпляру, тому вони не можуть використовувати параметр типу класу напряму.
class Storage<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
// Статичний метод має власний параметр типу U
static create<U>(value: U): Storage<U> {
return new Storage(value);
}
}
const numberStorage = Storage.create(100);
const textStorage = Storage.create("hello");
console.log(numberStorage.getValue()); // 100
console.log(textStorage.getValue()); // "hello"Метод create<U> оголошує власний параметр U, тому він може створювати екземпляри Storage<U> для різних типів.
Узагальнений клас доречний, коли:
алгоритм однаковий для різних типів даних;
потрібно зберігати стан певного типу;
методи повинні приймати та повертати узгоджені типи;
потрібно уникнути дублювання класів;
важливо зберегти перевірку типів без використання any.
Наприклад, один Collection<T> може працювати з:
числами;
рядками;
об’єктами користувачів;
об’єктами товарів.
При цьому кожен екземпляр зберігає власний конкретний тип.
any замість параметра типуclass UnsafeContainer {
constructor(public value: any) {}
}Такий клас дозволяє передавати несумісні значення, і TypeScript не зможе ефективно перевірити код.
Безпечніший варіант:
class SafeContainer<T> {
constructor(public value: T) {}
setValue(value: T): void {
this.value = value;
}
}Усі методи, що працюють зі станом T, повинні використовувати узгоджений тип:
class Box<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
setValue(value: T): void {
this.value = value;
}
getValue(): T {
return this.value;
}
}Якщо метод прийматиме any або неправильний тип, частина переваг узагальнення буде втрачена.
undefined у методах пошукуМетод, який може не знайти значення, повинен повертати об’єднання з undefined:
class List<T> {
constructor(private items: T[]) {}
find(index: number): T | undefined {
return this.items[index];
}
}Не слід оголошувати результат як просто T, якщо значення насправді може бути відсутнім.
Обмеження має містити лише властивості, які справді потрібні класу:
interface HasId {
id: number;
}
class Store<T extends HasId> {
findById(id: number): T | undefined {
// Реалізація може безпечно використовувати entity.id
return undefined;
}
}Не потрібно вимагати зайві властивості, якщо клас їх не використовує. Інакше він стане менш універсальним.
Узагальнений клас описується за допомогою параметра типу, наприклад class Box<T>.
Параметр типу можна використовувати у властивостях, конструкторах і методах.
Один клас може створювати екземпляри для різних типів.
TypeScript часто автоматично виводить тип із аргументів конструктора.
Клас може мати кілька параметрів типу: class KeyValueStore<K, V>.
За допомогою extends можна обмежити допустимі типи.
Метод може мати власний параметр типу, незалежний від параметра класу.
Узагальнення допомагають повторно використовувати код і зберігати статичну перевірку типів без any.