Где можно использовать generics
Перейти к содержимому

Где можно использовать generics

  • автор:

Для чего использовать дженерики в TypeScript

Дженерики (generic) помогают писать универсальный, переиспользуемый код, а также в некоторых случаях позволяют отказаться от any . Главная задача дженериков — помочь разработчику писать код, который одинаково будет работать со значениями разных типов.

Посмотрим на примере из реального мира.

Представьте завод по изготовлению автомобилей. Старый завод, который проектировался для сборки автомобиля определённой модели. На нём могут собирать только такую модель автомобиля, а если потребуется выпустить машину с немного другим кузовом, то придётся строить новый завод. Это неоптимальное решение. Если разные машины собираются одинаково, то лучше научиться собирать разные машины на одном заводе.

�� Узнайте больше о дженериках, научитесь на практике использовать аннотацию типов и обобщённое программирование на профессиональном курсе по TypeScript.

Суть дженериков

С дженериками тоже примерно так. Если мы напишем функцию и жёстко зададим тип, то она сможет работать только со значениями этого типа. Значения других типов передать не получится. Есть два способа это поправить.

Первый способ. Написать несколько одинаковых функций, которые работают с разными типами. Например, такая функция проверяет, есть ли в массиве конкретный элемент.

function includeStr(array: string[], query: string): boolean < // на входе массив и строка для поиска for (const value of array) < // перебираем массив if (value === query) < // если в массиве есть элемент — возвращаем true return true; >> // если ничего не нашлось, возвращаем false return false; > 

Функция будет отлично работать на массивах из строк. Но для поиска в массиве из чисел придётся дублировать функцию, менять типы, но сам код функции останется неизменным. Например:

function includeNumber(array: number[], query: number): boolean < // всё то же самое, только на входе числа for (const value of array) < if (value === query) < return true; >> return false; > 

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

❌ Пишем много функций для разных типов

✅ Объявляем в функции параметр типа, а потом передаём через него нужный тип

Вместо конкретного типа, мы как будто объявляем «переменную», а затем передаём в неё нужный тип. Таким образом, получается код, который может работать с разными типами:

function include < T >(array: T[], query: T): boolean < for (const value of array) < if (value === query) < return true; >> return false; > 

Код функции не поменялся, но теперь мы не указываем конкретный тип. Мы заводим переменную T и говорим, что тип параметра array — это тип, который будет передан в переменную T . А тип параметра query — это тип, который будет передан через переменную T .

Когда мы захотим воспользоваться этой функцией, то помимо данных для параметров array и query мы ещё должны передать информацию о типах (для переменной T ). В первом примере мы передаём тип string , а во втором — number .

// передаём string в качестве типа include < string >(['igor', 'sasha', 'ira'], 'ira'); // true // передаём number в качестве типа include < number >([1, 3, 5], 7); // false 

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

�� Дженерики — переменные, через которые мы можем передавать тип.

Ещё о JavaScript

  • Type predicates в TypeScript на примере
  • Типы данных в JavaScript. Инструкция для начинающих
  • Живые и неживые коллекции в JavaScript

«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.

Дженерики (универсальные типы) в TypeScript

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

Дженерики в TypeScript позволяют разработчикам писать многоразовый и гибкий код, абстрагируясь от типов. Используя дженерики, разработчики могут создавать функции, классы и интерфейсы, которые работают с любым типом, а не ограничиваются определённым типом. Возможность создавать компонент, который может работать с несколькими типами, а не только с одним, является одним из основных инструментов в наборе инструментов для создания повторно используемых элементов в таких языках программирования, как C# и Java. В результате пользователи могут использовать разные типы при использовании этих компонентов.

В этой статье представлена идея Дженериков в TypeScript и когда их использовать. В ней описывается, как можно использовать дженерики с функциями, классами, интерфейсами и типами, и приводятся примеры их использования в реальных ситуациях. Эти примеры чётко определены, поэтому вы можете следовать им в предпочитаемой вами интегрированной среде разработки.

Преимущества Дженериков

Список преимуществ, которые предлагают дженерики в TypeScript, выглядит следующим образом:

  • Мы можем безопасно хранить один объект, используя дженерики, не сохраняя другие типы.
  • Используя дженерики, мы можем избежать приведения типов любых переменных или функций во время вызова.
  • Дженерики обычно проверяются во время компиляции, чтобы гарантировать отсутствие проблем во время выполнения.
  • Дженерики могут помочь повысить производительность кода, позволяя TypeScript оптимизировать его для определённых типов данных, уменьшая необходимость проверки типов во время выполнения.
  • Использование дженериков делает код более читабельным и понятным, облегчая работу с ним и его сопровождение для других разработчиков.

Использование дженериков в функциях

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

Ниже приведён пример того, как написать функцию, которая возвращает первый элемент массива array . Вот пример функции возвращающей первый элемент массива в TypeScript с использованием дженериков:

function firstelementT>(arr: T[]): T  
return arr[0];
>

Чтобы использовать функцию с различными типами массивов, при вызове функции передаётся определённый тип. Например:

function firstElementT>(array: T[]): T | undefined  
return array[0];
>

const numbers = [1, 2, 3];
const firstNumber = firstElementnumber>(numbers);
console.log(firstNumber); // 1

const names = ["Daniel", "Micheal", "Charlie"];
const firstName = firstElementstring>(names);
console.log(firstName); // 'Daniel'

В этом примере функция используется с двумя массивами разных типов, numbers (массив чисел) и names (массив строк). Обходя определённый тип при вызове функции, функция может работать с соответствующим типом данных.

Использование дженериков в классах и интерфейсах

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

class StackT>  
private data: T[] = [];
push(item: T)
this.data.push(item);
>
pop(): T | undefined
return this.data.pop();
>
>

let numberStack = new Stacknumber>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
console.log(numberStack.pop()); // 1

let stringStack = new Stackstring>();
stringStack.push("a");
stringStack.push("b");
console.log(stringStack.pop()); // 'b'
console.log(stringStack.pop()); // 'a'

Чтобы использовать класс с разными типами данных, конкретный тип передаётся при создании экземпляра класса. Например:

let numbers = new Stacknumber>(); 
numbers.push(1);
numbers.push(2);
console.log(numbers.pop()); // 2
console.log(numbers.pop()); // 1

let names = new Stackstring>();
names.push("Alice");
names.push("Bob");
console.log(names.pop()); // 'Bob'
console.log(names.pop()); // 'Alice'

В этом примере класс используется для создания двух стеков разных типов: numbers (стек чисел) и names (стек строк). Класс может работать с соответствующим типом данных, передавая определённый тип при создании экземпляра класса.

Встроенный дженерик (универсальный тип) и интерфейсы

TypeScript предоставляет несколько встроенных универсальных типов и интерфейсов, таких как Array , Promise и Map , обычно используемых в JavaScript. Эти типы определяются с помощью дженерика (универсального типа) и могут использоваться с любым типом данных. Например, Array — это дженерик (универсальный тип), представляющий упорядоченную коллекцию элементов. Его можно использовать с любым типом данных, например числами или строками, минуя определённый тип при создании массива.

let numbers = [1, 2, 3]; 
let names = ['Daniel', 'Micheal', 'Charlie'];

Другим примером является тип Promise , представляющий значений, которое может быть ещё недоступно, но будет доступно в какой-то момент в будущем. Тип Promise также определяется с помощью дженерика (универсального типа), представляющего значение, которое будет доступно в будущем.

let promise: Promisestring> = new Promise((resolve, reject) =>  
setTimeout(() =>
resolve("Hello, World!");
>, 1000);
>);

promise.then((value: string) => console.log(value)); // 'Hello, World!'

Наконец, Map — это дженерик (универсальный тип), представляющий набор пар ключ-значение. Дженерики (универсальные типы) для ключей и значений могут быть разными.

let map = new Mapstring, number>(); 
map.set("Daniel", 25);
map.set("Michael", 30);
console.log(map.get("Daniel")); // 25

Помимо использования этих встроенных дженериков (универсальных типов) и интерфейсов, вы также можете расширить их для добавления дополнительных функций. Например, вы можете создать собственный класс Stack , расширяющий встроенный тип Array , как показано в предыдущем примере. Это позволяет использовать преимущества встроенной функциональности типа Array при добавлении пользовательских функций.

Заключение

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

Generics

Начиная с JDK 1.5, в Java появляются новые возможности для программирования. Одним из таких нововведений являются Generics. Generics являются аналогией с конструкцией «Шаблонов»(template) в С++, но имеет свои нюансы. Generics позволяют абстрагировать множество типов. Наиболее распространенными примерами являются Коллекции.

Вот типичное использование такого рода (без Generics):

1. List myIntList = new LinkedList(); 2. myIntList.add(new Integer(0)); 3. Integer x = (Integer) myIntList.iterator().next();

Как правило, программист знает, какие данные должны быть в List’e. Тем не менее, стоит обратить особое внимание на Приведение типа («Cast») в строчке 3. Компилятор может лишь гарантировать, что метод next() вернёт Object, но чтобы обеспечить присвоение переменной типа Integer правильным и безопасным, требуется Cast. Cast не только создает беспорядки, но дает возможность появление ошибки «Runtime Error» из-за невнимательности программиста.

И появляется такой вопрос: «Как с этим бороться? » В частности: «Как же зарезервировать List для определенного типа данных?»

Как раз такую проблему решают Generics.

1. List myIntList = new LinkedList (); 2. myIntList.add(new Integer(0)); 3. Integer x = myIntList.iterator().next();

Обратите внимание на объявления типа для переменной myIntList. Он указывает на то, что это не просто произвольный List, а List. Мы говорим, что List является generic-интерфейсом, который принимает параметр типа — в этом случае, Integer. Кроме того, необходимо обратить внимание на то, что теперь Cast выполняется в строчке 3 автоматически.

Некоторые могут задуматься, что беспорядок в коде увеличился, но это не так. Вместо приведения к Integer в строчке 3, у нас теперь есть Integer в качестве параметра в строчке 1. Здесь существенное отличие. Теперь компилятор может проверить этот тип на корректность во время компиляции.

И когда мы говорим, что myIntList объявлен как List, это будет справедливо во всем коде и компилятор это гарантирует.

Эффект от Generics особенно проявляется в крупных проектах: он улучшает читаемость и надежность кода в целом.

Свойства

  • Строгая типизация
  • Единая реализация
  • Отсутствие информации о типе

Пример реализации Generic-класса

public interface List  < E get(int i); set(int i, E e); add(E e); Iteratoriterator(); … >

Для того чтобы использовать класс как Generics, мы должны прописать после имени класса , куда можно подставить любое имя, wildcard и т.д.

После того как было объявлено имя generic-типа его можно использовать как обычный тип внутри метода. И когда в коде будет объявлен, к примеру, List, то Е станет Integer для переменной list (как показано ниже).

Теперь рассмотрим чем старая реализация кода отличается от новой:

List ─ список элементов E

List list = new List(); list.add(new Integer(1)); Integer i = (Integer) list.get(0);
List list = new List(); list.add(new Integer(1)); Integer i = list.get(0);

Как видите, больше не нужно приводить Integer, так как метод get() возвращает ссылку на объект конкретного типа (в данном случае – Integer).

Несовместимость generic-типов

Это одна из самых важных вещей, которую вы должны узнать о Generics

Как говорится: «В бочке мёда есть ложка дегтя». Для того чтобы сохранить целостности и независимости друг от друга Коллекции, у Generics существует так называемая «Несовместимость generic-типов».

Пусть у нас есть тип Foo, который является подтипом Bar, и еще G - наследник Коллекции. То G не является наследником G.
List li = new ArrayList(); List lo = li;
lo.add(“hello”); // ClassCastException: String -> int Integer li = lo.get(0);

Проблемы реализации Generics

  • Решение 1 — Wildcard

Пусть мы захотели написать метод, который берет Collection и выводит на экран. И мы захотели вызвать dump для Integer.

void dump(Collection c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Object o = i.next(); System.out.println(o); >>
List l; dump(l); List l; dump(l); // Ошибка 

В этом примере List не может использовать метод dump, так как он не является подтипом List.

Проблема в том что эта реализация кода не эффективна, так как Collection не является полностью родительской коллекцией всех остальных коллекции, грубо говоря Collection имеет ограничения.

Для решения этой проблемы используется Wildcard («?»). Он не имеет ограничения в использовании(то есть имеет соответствие с любым типом) и в этом его плюсы. И теперь, мы можем вызвать dump с любым типом коллекции.

void dump(Collection c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Object o = i.next(); System.out.println(o); >>
  • Решение 2 – Bounded Wildcard

Пусть мы захотели написать метод, который рисует List. И у Shape есть наследник Circle. И мы хотим вызвать draw для Circle.

void draw(List c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Shape s = i.next(); s.draw(); >>
List l; draw(l); List l; draw(l); // Ошибка 

Проблема в том, что у нас не получится из-за несовместимости типов. Предложенное решение используется, если метод который нужно реализовать использовал бы определенный тип и его подтипов. Так называемое «Ограничение сверху». Для этого нужно вместо прописать .

void draw(List c) < for (Iteratori = c.iterator(); i.hasNext(); ) < Shape s = i.next(); s.draw(); >>
  • Решение 3 – Generic-Метод

Пусть вы захотели сделать метод, который берет массив Object и переносить их в коллекцию.

void addAll(Object[] a, Collection c) < for (int i = 0; i < a.length; i++) < c.add(a[i]); >>
addAll(new String[10], new ArrayList()); addAll(new Object[10], new ArrayList()); addAll(new Object[10], new ArrayList()); // Ошибка addAll(new String[10], new ArrayList()); // Ошибка 

Напомним, что вы не можете просто засунуть Object в коллекции неизвестного типа. Способ решения этой проблемы является использование «Generic-Метод» Для этого перед методом нужно объявить и использовать его.

 void addAll(T[] a, Collection c) < for (int i = 0; i < a.length; i++) < c.add(a[i]); >>

Но все равно после выполнение останется ошибка в третьей строчке :

addAll(new Object[10], new ArrayList()); // Ошибка 
  • Решение 4 – Bounded type argument

Реализуем метод копирование из одной коллекции в другую

 void addAll(Collection c, Collection c2) < for (Iteratori = c.iterator(); i.hasNext(); ) < M o = i.next(); c2.add(o); >>
addAll(new AL(), new AL()); addAll(new AL(), new AL()); //Ошибка

Проблема в том что две Коллекции могут быть разных типов (несовместимость generic-типов). Для таких случаев было придуман Bounded type argument. Он нужен если метод ,который мы пишем использовал бы определенный тип данных. Для этого нужно ввести (N принимает только значения M). Также можно корректно писать . (Принимает значения нескольких переменных)

 void addAll(Collection c, Collection c2) < for (Iteratori = c.iterator(); i.hasNext(); ) < N o = i.next(); c2.add(o); >>
  • Решение 5 – Lower bounded wcard

Реализуем метод нахождение максимума в коллекции.

> T max(Collection c)

List il; Integer I = max(il); class Test implements Comparable  List tl; Test t = max(tl); // Ошибка
  • > обозначает что Т обязан реализовывать интерфейс Comparable.

Ошибка возникает из за того что Test реализует интерфейс Comparable. Решение этой проблемы — Lower bounded wcard(«Ограничение снизу»). Суть в том что мы будет реализовывать метод не только для Т, но и для его Супер-типов(Родительских типов). Например: Если мы напишем

List list;

Мы можем заполнить его List, List или List.

> T max(Collection c)

  • Решение 6 – Wildcard Capture

Реализуем метод Swap в List

void swap(List list, int i, int j) < list.set(i, list.get(j)); // Ошибка >

Проблема в том, что метод List.set() не может работать с List, так как ему не известно какой он List. Для решение этой проблемы используют «Wildcard Capture» (или «Capture helpers»). Суть заключается в том, чтобы обмануть компилятор. Напишем еще один метод с параметризованной переменной и будем его использовать внутри нашего метода.

void swap(List list, int i, int j) < swapImpl(list, i, j); > void swapImpl(List list, int i, int j)

Ограничения Generic

Также нужно запомнить простые правила для работы с Generics.

  • Невозможно создать массив параметра типа
Collection c; T[] ta; new T[10]; // Ошибка !!
  • Невозможно создать массив Generic-классов
new ArrayList>(); List[] la = new List[10]; // Ошибка !!

Преобразование типов

В Generics также можно манипулировать с информацией, хранящийся в переменных.

  • Уничтожение информации о типе
List l = new ArrayList();
  • Добавление информации о типе
List l = (List) new ArrayList(); List l1 = new ArrayList();

Примеры кода

  • Первый пример:
List ls; List li; ls.getClass() == li.getClass() // True ls instanceof List // True ls instanceof List // Запрещено
  • Второй пример:

Нахождение максимума в Коллекции Integer.

Collection c; Iterator i = c.iterator(); Integer max = (Integer) i.next(); while(i.hasNext()) < Integer next = (Integer) i.next(); if (next.compareTo(max) > 0) < max = next; >>
  • С помощью Generics
Collection c; Iterator i = c.iterator(); Integer max = i.next(); while(i.hasNext()) < Integer next = i.next(); if (next.compareTo(max) >0) < max = next; >>

Дженерики в TypeScript

Привет, я Сергей Вахрамов, занимаюсь фронтенд-разработкой на Angular в компании Тинькофф. Во фронтенд-разработку вошел напрямую с тайпскрипта, просто перечитав всю документацию. С того момента и спецификация ECMAScript расширилась, и TypeScript сильно подрос. Казалось бы, почему разработчики могут бояться дженериков, ведь бояться там нечего? Мой опыт общения с джуниор-разработчиками говорит, что во многом ребята не используют обобщенные типы просто потому, что кто-то пустил легенду об их сложности.

Эта статья для тех, кто не использует generic-типы в TypeScript: не знают о них, боятся использовать или используют вместо реальных типов — any .

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

Посмотрим на пример использования дженериков в TypeScript. Представьте, что у нас есть массив значков валют. В JavaScript мы бы просто написали:

const currencySigns = ['₽', '€', '£'];

В TypeScript с помощью дженериков можно написать:

type CurrencySign = '₽' | '€' | '£'; const currencySigns: ReadonlyArray = ['₽', '€', '£'];

Здесь важно уделить внимание типу переменной currencySigns — ReadonlyArray , обобщенный тип, означает «неизменяемый массив», при этом мы говорим языку, что в нем могут лежать только элементы типа CurrencySign , это параметр дженерика.

Ничто не запрещает написать ReadonlyArray , но часто типы разделяют, чтобы в будущем их было удобно использовать отдельно друг от друга. Например, как в данном случае, было бы удобно заранее иметь тип элемента массива и уже из него сконструировать другой тип, двигаясь «от меньшего к большему», а не выделять из большего типа меньший. Это возможно с помощью декларации infer, но об этом поговорим в другой раз.

Оператор keyof

Это оператор, который берет все ключи объекта и представляет в виде числового или строкового литерального объединения.

Давайте представим, что нам с сервера шлют объект с такой структурой:

type Payment =

Если нам потребуются ключи из типа Payment, тут и пригодится оператор keyof.

type ObjectKey = keyof Obj;

В итоге получим:

type PaymentKeys = ObjectKey; // 'amount' | 'currency' | 'currencySign'

ObjectKey — это дженерик-тип (обобщенный тип), который перечисляет ключи объекта Obj , переданного в него как в параметр.

const key: PaymentKeys = 'amount'; // OK const key: PaymentKeys = 'from'; // Ошибка, такого ключа у Payment нет

Что дальше?

Мы дали основные знания, которые помогут уверенно использовать мощь обобщенных типов. Теперь можно переходить к Generic Types.

Случается, что в работе мы используем уже написанное до нас и не влияем на доработку кода.

Представим, что нам с сервера приходит описание платежа из истории в формате:

type PaymentInfo =

Потом кто-то разработал новый сервис, который отдает информацию о переводах. И он стал передавать информацию в следующем виде:

type NewPaymentInfo = < id: string; amount: number; currency: number; // код валюты >

Теперь нам приходит код валюты, а не ее буквенное обозначение. При этом на старые записи в истории мы все еще получаем строковый код. Чтобы не описывать разные типы и не создавать путаницу, можно объединить их в один обобщенный тип — дженерик:

type PaymentInfo = < // T — параметр дженерика id: string; amount: number; currency: T; // «настраиваем» тип поля currency >const paymentInfo: PaymentInfo = // …

Можно указать типы параметров дженерика по умолчанию. Если не передать в такой дженерик параметр, то TypeScript возьмёт значение по умолчанию:

type PaymentInfo = < … >// T — по умолчанию тип string const paymentInfo: PaymentInfo = // … тип переменной — PaymentInfo

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

Почему T ? Так сложилось, что параметры дженериков именуют одной буквой (T означает Type), но вы без проблем можете написать не T , а Currency :

type PaymentInfo =

При этом, если в IDE мы попытаемся в paymentInfo присвоить полю currency значение типа number , получим ошибку: TypeScript уже охраняет нас. Этого бы не было, если бы тип поля currency был просто string | number . Ведь мы дали возможность разработчику с помощью параметра указать, значение какого типа будет лежать в поле currency .

Интерфейсы тоже могут быть обобщёнными:

interface PaymentInfo

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

Типизация функций и методов

Некоторые из функций могут быть вызваны с разным количеством аргументов и их типами. В TypeScript такие функции можно описать с помощью перегрузок.

Допустим, у нас есть функция identity — она возвращает аргумент, который мы ей передали.

function identity(arg: string): string; function identity(arg: number): number; function identity(arg: unknown[]): unknown[]

Прошло время, и нам понадобилось сделать такую функцию для boolean . Мы побежим в файл, чтобы добавить еще одну перегрузку:

function identity(arg: boolean): boolean;

Этот вариант страдает от возможности адекватной расширяемости: если понадобится такая функция identity , которая будет работать с Payments[] , нам потребуется добавить еще одну сигнатуру. А если таких типов десяток или сотня? Писать 100 сигнатур — не выход из ситуации.

Но можно написать вот так:

function identity(arg: T): T

В этом примере мы просто типизировали функцию (Function Declaration) через дженерик. Можно также типизировать функциональное выражение (Function Expression):

const identity = (arg: T): T

Типизация классов

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

Для упрощения представлю класс IdentityClass .

Код на JavaScript:

class IdentityClass < constructor(value) < this.value = value; >getIdentity() < return this.value; >>

Тот же самый класс будет выглядеть намного понятнее с TypeScript. Для начала опишем интерфейс:

interface IdentityGetter

Теперь напишем класс, который реализует наш интерфейс:

class IdentityClass implements IdentityGetter  < constructor(private readonly value: T) < this.value = value; >getIdentity(): T < return this.value; >>

Ограничения дженериков. Generic Constraints

Иногда нужно как-то ограничить тип, принимаемый дженериком. Покажу на реальном примере.

Допустим, у нас есть функция для получения значения свойства length аргумента:

function getLength(arg: T): number

Если вы попробуете ее скомпилировать, получите ошибку:

Property 'length' does not exist on type 'T'.

Происходит это потому, что TypeScript не знает, есть ли у передаваемого аргумента свойство length . Это легко исправить с помощью Generic Constraint — ограничения дженерика. Создадим тип и укажем функции, что при типизации она может принимать только такой тип, который имеет свойство length типа number :

interface Lengthwise < length: number; >function getLength(arg: T): number

Ошибка пропадет, а пример заработает. При этом разработчик, использующий функцию, будет точно понимать, какой аргумент требуется в нее передать.

getLength(['Я', 'люблю', 'Тинькофф']) === 3 getLength('Я люблю Тинькофф') === 16 getLength(1027739642281) // Ошибка, у number нет свойства length

Какой еще может быть практический пример? Давайте напишем функцию, которая принимает в себя объект и ключ, а выдает значение из переданного объекта по ключу:

function getPropertyValue(obj: Obj, key: Key): Obj[Key]

В этом примере также показана возможность ограничения типа, используемого в объявлении функции, с помощью уже имеющегося параметра:

function getPropertyValue(. ) < … >// тип Key ограничен типом keyof Obj 
const developer = < name: 'Sergey Vakhramov', nickname: 'vakhramoff', website: 'vakhramoff.ru', >getPropertyValue(developer, 'nickname') === 'vakhramoff' getPropertyValue(developer, 'age') // Ошибка, у объекта в переменной developer нет свойства age

Охранники типов: Type Guards

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

Оператор typeof (typeof type guards). TypeScript — классный инструмент. Он позволяет выводить типы из конкретных переменных. Это называется Type Inference или «вывод типов».

Допустим, у нас уже есть переменная со ссылкой на объект, в котором записаны данные:

const account = < amount: 1_000_000, currency: 'RUB', currencySign: '₽', locked: false, >;

Если мы захотим вывести тип этой переменной в коде, то без проблем сделаем это при помощи typeof :

type Account = typeof account; // TypeScript сам выведет следующий тип: // Account = < // amount: number; // currency: string; // currencySign: string; // locked: boolean; // >

В JavaScript тоже есть оператор typeof , который позволяет узнать тип значения, который хранится в переменной.

typeof 'Hello, world!' === 'string'; typeof 1_234_567 === 'number'; typeof < nickname: 'vakhramoff' >=== 'object'; typeof ['₽', '€', '£'] === 'object'; // подумайте, почему так typeof null === 'object'; // засада!

Почему typeof null === ‘object’? Это общепризнанное поведение JS. Многие считают его багом, попытаюсь объяснить почему.

Любая переменная хранит свое значение в виде последовательности битов. Из 32 бит, отведенных для хранения значения переменной, решили 3 выделить под хранение ее типа.

Мы понимаем, что всего возможно 2^3 = 8 вариантов типов и, по счастливой случайности, 000 выделили для типа object . Если вы когда-то встречались с понятием «нулевой указатель», то знаете, что он представляет собой переменную — последовательность нулей. Догадываетесь? Проверяя у этой переменной тип, оператор typeof в JavaScript встречает три нулика и понимает, что перед нами объект.

Оператор instanceof (instanceof type guards). Позволяет проверить, является ли данный объект экземпляром конкретного класса.

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

class Account <> class PremiumAccount extends Account <> class Currency <> const account = new Account(); const premiumAccount = new PremiumAccount(); const currency = new Currency(); account instanceof Account === true account instanceof PremiumAccount === false premiumAccount instanceof Account === true premiumAccount instanceof PremiumAccount === true currency instanceof Account === false

User-Defined Type Guards

В TypeScript есть еще один прекрасный инструмент — «определенные пользователем тайп-гарды».

Допустим, у нас есть следующие интерфейсы:

interface Pet < name: string; >interface Cat extends Pet < meow(): void; >interface Dog extends Pet

Нам нужно написать функцию, которая будет определять, является ли переданное животное объектом, реализующим интерфейс Dog . Если мы напишем в классическом стиле, то это вызовет ряд проблем — TypeScript не будет понимать, что перед ним точно объект, соответствующий интерфейсу Dog :

function isDog(pet: Pet): boolean < return (pet as Dog).bark !== undefined && typeof (pet as Dog).bark === 'function'; >const pet: Pet = < name: 'Wolfgang', bark: () =>console.log('Гав-гав!'), > if (isDog(pet)) < pet.bark(); // Ошибка! TypeScript не понимает, что pet — это Dog >

Мы можем легко это исправить. Достаточно лишь сказать TypeScript, что наша функция определяет, реализует ли переданный аргумент интерфейс Dog :

function isDog(pet: Pet): pet is Dog

Теперь TypeScript не ругается, он понял тип переменной pet :

if (isDog(pet)) < pet.bark(); // OK, pet это Dog >

Так же тайп-гард будет работать при условном ветвлении во время проверки наличия поля с помощью оператора in , если тип является объединением, и при этом условное выражение однозначно подразделяется на ветви true и false , это позволяет тайпскрипту однозначно сузить тип внутри этих условных ветвей:

function makeNoise(pet: Cat | Dog): void < if ('meow' in pet) < return pet.meow(); // тип pet «сужается» до Cat >return pet.bark(); // тип pet «сужается» до Dog >

Сначала это может сложно восприниматься, поэтому подробнее про сужение типов предлагаю прочитать отдельно в документации.

Если искать примеры в реальном мире, то до определенной версии в jQuery невозможно было использовать метод isUndefined , который возвращал значение boolean вместо тайп-гарда . is undefined . Хотя этот метод в тайпскрипте и был, но разработчики jQuery не описали возвращаемое функцией значение должным образом. Это могло сильно мешать при разработке.

Также хочу отметить, что при использовании Type Guard вся ответственность за определение типов лежит на разработчике. Он напрямую говорит TypeScript: «Это точно вот этот тип и никакой другой, я гарантирую».

В заключение

Мы узнали про дженерики и их параметризацию, научились с помощью них типизировать переменные, функции и методы, а также классы. Узнали, как можно ограничить типы и при необходимости помочь TypeScript с выведением типов, использовав Type Guard.

Если вы только начинаете использовать TypeScript в своей работе, начните с чтения официальной документации. Ее стоит изучить в первую очередь, просто сев и почитав несколько часов или, возможно, дней. Тогда сложится полное представление о языке. Вряд ли запомнится все и сразу, но уже будете знать, где что искать.

Для продвинутых пользователей языка рекомендую официальную книгу — TypeScript Handbook. Ее можно использовать как шпаргалку с паттернами, объяснением поведения системы вывода типов, описанием работы компилятора. Там раскрываются тонкие моменты, о которых разработчик не задумывается повседневно.

Если вы «познали дзен» в написании дженериков или просто хотите попрактиковаться на реальных примерах и набить руку, можете также порешать задачки Type Challenges в одноименном github-репозитории. В папке questions задачи разделены по сложности и пронумерованы. Под каждой есть ссылка на предлагаемые разработчиками решения — можете легко проверить себя.

  • Блог компании TINKOFF
  • Веб-разработка
  • JavaScript
  • TypeScript

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *