Статическая типизация защищает код от большого класса ошибок, связанных с неправильным использованием типов. Но у всего есть своя цена. В некоторых ситуациях достаточно добавить описание типов, в некоторых приходится вводить новые и не всегда простые понятия, например, дженерики. В этом уроке мы начнем знакомиться с ними.
Представим функцию слияния двух массивов. На JavaScript этот код записывается достаточно просто:
const merge = (coll1, coll2) => {
const result = [];
result.push(...coll1);
result.push(...coll2);
return result;
};
merge([1, 2], [3, 4]); // [1, 2, 3, 4]
merge(['one', 'two'], ['three']); // ['one', 'two', 'three']
Удобство динамической типизации тут проявляется в том, что эта функция автоматически работает для любых массивов, что бы в них ни хранилось.
В статически типизированных языках такой трюк не пройдет. Придется указывать конкретный тип:
function merge(coll1: number[], coll2: number[]): number[] {
const result = [];
result.push(...coll1);
result.push(...coll2);
return result;
}
merge([1, 2], [3, 4]); // [1, 2, 3, 4]
Если нужно сливать массивы, состоящие из строк, то придется использовать перегрузку функций. Но внутри возникнет проблема с возвращаемым типом, который будет разный в зависимости от входных параметров:
function merge(coll1: number[], coll2: number[]): number[];
function merge(coll1: string[], coll2: string[]): string[];
В языках с настоящей перегрузкой функций проблема будет заключаться в том, что появится много функций, у которых одинаковое тело. То есть по сути дублирование логики для всех возможных входных типов. Эта ситуация настолько распространенная и непростая, что для нее создана целая подсистема в системе типов. Она называется дженериками.
Дженерики в применении к функциям — это механизм, позволяющий создать такие функции, которые имеют одинаковую логику обработки для разных типов данных. Иногда такие функции называют обобщенными функциями.
Ниже пример реализации функции merge()
в обобщенном виде:
// или так
// function merge<T>(coll1: T[], coll2: T[]): T[]
function merge<T>(coll1: Array<T>, coll2: Array<T>): Array<T> {
// Тело функции не поменялось!
const result = [];
result.push(...coll1);
result.push(...coll2);
return result;
}
// Работает с массивами любых типов
// Сами массивы должны иметь совпадающий тип
merge([1, 2], [3, 4]); // [1, 2, 3, 4]
merge(['one', 'two'], ['three']); // ['one', 'two', 'three']
Здесь мы видим новый синтаксис, к которому нужно привыкнуть. Если не вдаваться в детали, запись в <T>
после имени функции говорит о том, что перед нами дженерик, который параметризуется типом T. Тип T означает, что мы могли бы использовать любую другую заглавную букву, например, X. Чаще всего мы будем видеть это обозначение, потому что это общепринятая практика.
Что конкретно скрывается под типом с точки зрения кода дженерика — не важно. Это может быть объект, число, строка или булево значение. В вызовах примера выше это число для первого вызова и строка для второго. Так же можно было бы сделать вызов с булевыми значениями:
merge([true], [false, false]); // [true, false, false]
Дальше уже внутри функции мы видим, что логика работы одинакова для всех типов и не зависит от типа. Мы просто перекладываем элементы массивов в другой массив. В этом месте код выглядит уже привычно.
Осталось разобраться с параметрами и возвращаемым значением.
Запись Array<T>
описывает обобщенный массив — тоже дженерик, но уже для типа. На месте этого параметра может оказаться любой массив, например, number[]
или boolean[]
. Соответственно, в коде функции мы говорим о том, что ожидаем на вход два массива одного типа, и этот же тип является выходным.
Имя параметра типа T имеет тут важную роль. Если бы мы использовали другую букву, то ее нужно было бы поменять для всех частей внутри:
function merge<X>(coll1: Array<X>, coll2: Array<X>): Array<X>
Так TypeScript понимает, что типы входных массивов и результирующего совпадают. То есть не получится вызвать эту функцию, передав туда одновременно массив из чисел и строк.
const result = merge([1, 2], ['wow']); // Error!
Но типы могут и не совпадать. Ниже пример дженерика, который возвращает первый элемент любого массива и null
, если он пустой:
function first<T>(coll: Array<T>): T | null {
return coll[0] ?? null;
}
first([]); // null
first([3, 2]); // 3
first(['code-basics', 'hexlet']); // code-basics
Дженерики — это большая тема, которая раскрывается в следующих уроках. Сейчас мы только познакомились с общей концепцией и постепенно начинаем ее использовать.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.