Дженерики определенно кажутся устрашающими любому, кто к ним не привык. В этом сообщении в блоге я постараюсь сделать их менее загадочными и покажу, как они могут быть полезны, используя более практический подход.

Вы можете продолжить на Машинописной площадке, если у вас не настроена локальная среда. Я бы порекомендовал набирать примеры на отдельной вкладке.
Первый взгляд
Обобщения, как следует из названия, помогают нам создавать больше многоразовых фрагментов кода.
Простыми словами:
Они могут помочь нам создавать четко определенные классы, интерфейсы и функции, при этом давая нам возможность сохранять их «универсальными», заставляя их работать в соответствии с переданным типом. (или типы) программистом.
Давайте посмотрим на это на нашем первом примере, вы можете ввести это в Игровую площадку машинописного текста:
function iTakeAnyArrays(arr: any[]): any[] {
return arr;
}
iTakeAnyArrays(1); //error
function iTakeGenericArrays<T>(arr:T[]): T[] {
return arr;
}
iTakeGenericArrays(1); //error
For now think of the <T> here as a kind of paceholder which will be replaced by a type passed in by the user (or a type that can be inferred from the arguments). As we proceed this will become clearer.
Мы намеренно передали нашим функциям неверный аргумент в приведенном выше примере, конечно, ожидается, что аргумент будет массивом. Это сделано для того, чтобы взглянуть на то, что может происходить за кулисами.
Если навести курсор на ошибки в редакторе (или на игровой площадке):
iTakeAnyArrays(1): Argument of type 'number' is not assignable to parameter of type 'any[]' iTakeGenericArrays(1): Argument of type 'number' is not assignable to parameter of type 'unknown[]'
Если вы не знаете разницы между любым и неизвестным, просто знайте, что неизвестно будет сужаться до предполагаемого типа всякий раз, когда его можно вывести. из кода, а затем придерживайтесь этого типа.
Вот подробнее об этом.
Однако «любой» никогда не сузится до типа и всегда будет произвольным.
Вот почему «любой» может иногда не подходить для обобщения частей вашего кода, поскольку вы теряете часть (или много) определения типа.
Если теперь мы попытаемся передать правильный тип аргументов:
function iTakeAnyArrays(arr: any[]): any[] {
return arr;
}
iTakeAnyArrays([1]);
function iTakeGenericArrays<T>(arr:T[]): T[] {
return arr;
}
iTakeGenericArrays([1]);
Ошибки, конечно, исчезли, и более интересная часть снова возникает, когда вы наводите курсор на следующее (я прокомментировал, что вы увидите в среде IDE при наведении курсора):
iTakeAnyArrays([1]); // function iTakeAnyArrays(arr: any[]): any[] iTakeGenericArrays([1]); // function iTakeGenericArrays<number>(arr: number[]): number[]
Если мы дополнительно изменим код, чтобы он принимал различные типы массивов, и наведем курсор на вызовы функций:
function iTakeAnyArrays(arr: any[]): any[] {
return arr;
}
iTakeAnyArrays([1]);
// function iTakeAnyArrays(arr: any[]): any[]
iTakeAnyArrays(['A']);
// function iTakeAnyArrays(arr: any[]): any[]
function iTakeGenericArrays<T>(arr:T[]): T[] {
return arr;
}
iTakeGenericArrays([1]);
// function iTakeGenericArrays<number>(arr: number[]): number[]
iTakeGenericArrays(['B']);
// function iTakeGenericArrays<string>(arr: string[]): string[]
Вы увидите, что наша функция iTakeGenericArrays корректирует определение в соответствии с типом, который можно вывести из аргументов.
Функция iTakeGenericArrays называется универсальной, поскольку теперь она работает с рядом типов.
Это можно было сделать, используя «T» в определении функции:
function iTakeGenericArrays<T>(arr:T[]): T[]
Это называется переменной типа и используется для простой передачи типа. Эта переменная работает только для передачи типов, но не значений. Вам не обязательно использовать букву «T» для имени переменной, и вы можете использовать любую другую переменную по вашему выбору.
До сих пор мы явно не передавали какой-либо тип, и компилятор TypeScript автоматически выводит его из аргументов.
При необходимости мы можем явно передавать типы. Мы немного изменили нашу предыдущую универсальную функцию, чтобы теперь возвращать длину переданного в массиве:
function iTakeGenericArrays<T>(arr:T[]): number {
return arr.length;
}
// T will be replaced by type string:
let arrLength = iTakeGenericArrays<string>(['B']);
console.log(arrLength);
// The following will give an error:
arrLength = iTakeGenericArrays<string>([2]);
console.log(arrLength);
// Type 'number' is not assignable to type 'string'
Мы использовали параметр функции arr: T [], чтобы добраться до этой точки, чтобы получить доступ к свойству массивов .length.
Вы можете задаться вопросом, почему мы не могли просто использовать следующее, напрямую заменив T чем-то вроде string []:
function iTakeGenericArrays<T>(arr:T): number {
return arr.length;
}
// T will be replaced by string:
let arrLength = iTakeGenericArrays<string[]>(['B']);
console.log(arrLength);
Here we have written
iTakeGenericArrays<T>(arr:T): number
instead of
iTakeGenericArrays<T>(arr:T[]): number
to see if we can directly pass in string[] into T, and if our compiler will accept it
Но это даст ошибку:
... return arr.length; ... Property 'length' does not exist on type 'T'.
Что ж, машинописный текст регистрирует эту ошибку, потому что он должен убедиться, что все типы передаются согласованно.
Если кто-то передаст тип число в переменную типа, то очевидно, что свойство .length для него не будет доступно. Следовательно, он гарантирует, что параметры нашей функции правильно определены и подходят для всех типов, которые могут быть переданы.
Использование нескольких параметров типа
Мы также можем использовать переменные нескольких типов в нашей универсальной функции. Это можно сделать просто так:
function multipleGenericTypes<T,K>(arg1: T, arg2: K): boolean {
return typeof arg1 === typeof arg2;
}
const res = multipleGenericTypes<string, string>('a', 'b');
console.log(res); // true
const anotherRes = multipleGenericTypes<string, number>('1', 1);
console.log(anotherRes); //false
Мы просто передаем переменную дополнительного типа как ‹T, K› и используем ее, как обычно, в наших аргументах.
Дженерики с ограничениями
Итак, до сих пор мы видели, что наши переменные типа могут принимать все типы данных, и код, который мы пишем с использованием этой переменной типа, также должен соответствовать совместимости со всеми типами.
Мы видели это ранее, когда пытались получить доступ к .length для переменной типа, которая также вполне могла быть числом, а не массивом.
Это может быть недостатком в некоторых случаях, когда, например, мы знаем, что наша функция будет иметь дело с диапазоном типов.
Практический пример
Давайте посмотрим на это на практике. Игры - одно из моих любимых увлечений, поэтому в нашем примере мы поговорим о том, что с этим связано.
Представьте, что у нас есть сценарий, в котором персонаж атакует другого персонажа. Пострадавшему будет нанесен ущерб. Обычный случай с реалистичными играми заключается в том, что предметы, используемые персонажами, также ухудшаются по мере использования.
Поэтому мы могли бы использовать общую функцию для нанесения урона персонажу, а затем повторно использовать ту же функцию, чтобы отразить ухудшение состояния оружия в результате этой атаки.
Поэтому нам нужно будет ограничить нашу универсальную функцию, чтобы она принимала только диапазон типов, в которых мы можем отразить какой-то ущерб для определенной характеристики. В играх часто встречается показатель hp (очко здоровья).
Поэтому мы попытаемся создать универсальную функцию, которая принимает любые типы, обладающие свойством hp.
Для этого давайте сначала создадим интерфейс:
interface CanTakeDamage {
hp: number;
}
Этот интерфейс определяет тип для любых объектов в нашей игре, которые могут иметь свойство hp.
После этого давайте определим классы для наших персонажей и оружия, которое они могут использовать. Оружие будет иметь некоторые общие свойства с Персонажами:
class Character {
name: string;
attack: number;
hp: number;
constructor(
name: string,
attack: number,
hp: number,
) {
this.name = name;
this.attack = attack;
this.hp = hp;
}
}
class Weapon extends Character {
price: number;
constructor(
name: string,
attack: number,
hp: number,
price: number,
) {
super(
name,
attack,
hp
);
this.price = price;
}
}
Таким образом, обе эти сущности будут иметь имя, базовый показатель атаки и показатель hp. Оружие дополнительно будет иметь свойство «цена».
Давайте теперь получим три таких игровых Сущности: ведьмака, Bruxa, на которую ведьмак будет нападать, и Меч, который он будет использовать для атаки.
Ведьмак - это «Геральт», Bruxa - это «Лилия», а Меч - великий «Aerondight».
const Witcher = new Character(
"Geralt",
1250,
3200
);
const Bruxa = new Character(
"Lily",
1450,
4200
);
const SilverSword = new Weapon(
"Aerondight", // name
550, // base attack
500, // hp property used here for sword "health"
5500 // price
);
Мы передаем каждому их соответствующую статистику вместе с их именем.
Отлично, теперь осталось только создать общую функцию, которая поможет нам нанести ущерб этим игровым объектам.
Эта функция может использовать переменную типа «T», но как сузить диапазон этой переменной типа, чтобы гарантировать, что только то, что имеет свойство hp, будет считаться допустимым типом?
Это можно сделать с помощью интерфейса, который мы определили ранее, если наша переменная типа расширит этот интерфейс:
interface CanTakeDamage {
hp: number;
}
...
function inflictDamage<T extends CanTakeDamage>(
victimEntity: T,
damage:number
) {
return victimEntity.hp - damage;
}
Таким образом, мы можем сузить диапазон допустимых типов до только тех, которые могут передаваться как CanTakeDamage.
Вышеупомянутая функция inflictDamage будет принимать только аргументы для параметра VicTakeDamage, который будет иметь тип CanTakeDamage, то есть у них будет hp собственность присутствует на них.
Это связано с тем, что мы указали, что любой тип, переданный в T, должен расширять CanTakeDamage.
<T extends CanTakeDamage>
Если бы мы не расширили этот интерфейс, мы могли бы передать любой произвольный тип и получить от компилятора ошибку о том, что свойство hp не присутствует в T. Поскольку T может быть задан любой другой тип, например строка или число.
Теперь все, что осталось сделать, это протестировать нашу функцию. К коду, который у нас есть, мы добавим следующее:
...
const damageBruxa = inflictDamage(
Bruxa,
Witcher.attack + SilverSword.attack
);
const damageSword = inflictDamage(
SilverSword,
Bruxa.hp/50
);
console.log(`
Bruxa now has ${damageBruxa} hp,
Sword degrades to ${damageSword} points
`);
Which will log:
Bruxa now has 2400 hp,
Sword degrades to 416 points
Ваш полный код должен выглядеть следующим образом:
Раз уж мы затронули тему интерфейсов и универсальных шаблонов, давайте теперь рассмотрим кое-что, что еще больше сближает их.
Общие интерфейсы
Предположим, у нас есть функция для проверки длины переданного массива:
function checkLength<T>(item: T[]): number {
return item.length;
};
У нас есть еще одна функция, в которую нам нужно передать это как обратный вызов. Эта другая функция также примет строку в качестве второго аргумента и будет использовать обратный вызов для возврата длины этой строки.
Что-то вроде следующего, но с отсутствующей частью:
function checkLength<T>(item: T[]): number {
return item.length;
}
function getStrLength(
cb: ??????,
str: string
): number {
return checkLength(str.split(''));
}
const len = getStrLength(
checkLength,
'The Lesser Evil.'
);
console.log(len);
Вопросительные знаки поставлены намеренно. Какой тип мы можем передать нашей функции обратного вызова checkLength?
Взглянув на нашу функцию checkLength, мы можем записать тип обратного вызова как:
function checkLength<T>(item: T[]): number {
return item.length;
}
function getStrLength(
cb: <T>(item: T[]) => number,
str: string
): number {
return cb<string>(str.split(''));
}
const len = getStrLength(
checkLength,
'The Lesser Evil.'
);
console.log(len); // 16
Наша функция имеет тип:
checkLength: <T>(item: T[]) => number since it takes in an item parameter of an array of any T type of elements, and returns a number which is the length of the array.
Запустив код, мы получаем ожидаемый результат.
Теперь предположим, что у нас есть другой случай, когда нам нужно проверить разницу в длине между двумя строками.
Опять же, используя переданный обратный вызов и две строки для проверки друг друга.
Мы можем повторно использовать наш метод checkLength для части обратного вызова, чтобы создать функцию getStrDiff. Примерно так:
function checkLength<T>(item: T[]): number {
return item.length;
};
function getStrLength(
cb: <T>(item: T[]) => number,
str: string
): number {
return cb<string>(str.split(''));
}
function getStrDiff(
cb: <T>(item: T[]) => number,
str1: string,
str2: string
): number {
return (
cb<string>(str1.split('')) - cb<string>(str2.split(''))
);
}
const len = getStrLength(
checkLength,
'The Lesser Evil.'
);
const diff = getStrDiff(
checkLength,
'Silver for Monsters',
'Steel for Humans'
)
console.log(len, diff); // 16, 3
Обратите внимание, как нам пришлось снова записать весь тип для обратного вызова. Этого, безусловно, следует избегать.
И эту проблему можно решить с помощью интерфейса. Мы можем определить один для типа этого общего обратного вызова:
interface CheckLength {
<T>(item: T[]): number
}
(Note the capital "C", this is not the same as our checkLength function earlier)
Теперь мы можем использовать этот интерфейс для определения типа обратного вызова в параметрах нашей функции как для getStrDiff, так и для getStrLength:
interface CheckLength {
<T>(item: T[]): number
}
function checkLength<T>(item: T[]): number {
return item.length;
};
function getStrLength(
cb: CheckLength,
str: string
): number {
return cb<string>(str.split(''));
}
function getStrDiff(
cb: CheckLength,
str1: string,
str2: string
): number {
return (
cb<string>(str1.split('')) - cb<string>(str2.split(''));
);
}
const len = getStrLength(
checkLength,
'The Lesser Evil.'
);
const diff = getStrDiff(
checkLength,
'Silver for Monsters',
'Steel for Humans'
)
console.log(len, diff);
Мы также можем передать переменную типа самому интерфейсу, чтобы улучшить наш код:
interface CheckLength<T> {
(item: T[]): number
}
function checkLength<T>(item: T[]): number {
return item.length;
};
function getStrLength(
cb: CheckLength<string>,
str: string
): number {
return cb(str.split(''));
}
function getStrDiff(
cb: CheckLength<string>,
str1: string,
str2: string
): number {
return (
cb(str1.split('')) - cb(str2.split(''));
);
}
const len = getStrLength(
checkLength,
'The Lesser Evil.'
);
const diff = getStrDiff(
checkLength,
'Silver for Monsters',
'Steel for Humans'
)
console.log(len, diff);
Чем это отличается от предыдущего кода? Что ж, раньше единственным преимуществом, которое у нас было, было сделать определение типа для параметра cb более кратким.
Раньше нам еще приходилось выписывать:
function getStrLength(
cb: CheckLength,
str: string
): number {
return cb<string>(str.split(''));
}
function getStrDiff(
cb: CheckLength,
str1: string,
str2: string
): number {
return (
cb<string>(str1.split('')) - cb<string>(str2.split(''));
);
}
А сейчас
function getStrLength(
cb: CheckLength<string>,
str: string
): number {
return cb(str.split(''));
}
function getStrDiff(
cb: CheckLength<string>,
str1: string,
str2: string
): number {
return (
cb(str1.split('')) - cb(str2.split(''));
);
}
Мы заставляем тип T быть строкой для обратного вызова cb, с помощью самого интерфейса.
Вот полный код:
В заключение давайте посмотрим, как мы можем использовать дженерики с классами в другом практическом приложении.
Общие классы
Если вы до сих пор понимали концепции, понимание общих классов должно быть легким.
Как и в случае с интерфейсами, мы просто добавляем переменную типа после объявления класса:
class Character<T> {
private name: T;
sayMessage: (msg: T) => T;
constructor(
name: T,
sayMessage: (msg: T) => T
) {
this.name = name;
this.sayMessage = sayMessage;
}
sayName = (): T => {
return this.name;
};
}
Затем мы можем создать объект Character следующим образом:
const Geralt = new Character<string>(
'Geralt of Rivia',
(msg) => {
return msg
}
);
T в этом случае будет принимать строку в качестве своего типа, и поэтому метод sayMessage в нашем объекте Geralt будет принимать только строки в качестве своего аргумента msg.
const geraltSays = Geralt.sayMessage(`What is up my dude, I am ${Geralt.sayName()}`);
console.log(geraltSays);
Will log
"What is up my dude, I am Geralt of Rivia"
Whereas
const geraltSays = Geralt.sayMessage(10);
console.log(geraltSays);
// will give an error since 10 is not of type string
Полный код выглядит так:
И это должно охватывать большую часть того, что вам нужно знать, чтобы начать работу с дженериками.
Я бы порекомендовал заглянуть в Руководство по машинописному тексту, чтобы узнать больше о типах и машинописных текстах в целом.
Надеюсь, это каким-то образом помогло, по крайней мере, лучше понять дженерики!
Уровень кодирования
Спасибо, что стали частью нашего сообщества! Подпишитесь на наш канал YouTube или присоединитесь к Интервью по программированию Skilled.dev.