Статическая типизация защищает код от большого класса ошибок, связанных с неправильным использованием типов. Но у всего есть своя цена. В некоторых ситуациях достаточно добавить описание типов, в некоторых приходится вводить новые и не всегда простые понятия, например, дженерики. В этом уроке мы начнем знакомиться с ними.
Представим функцию слияния двух массивов. На 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.length > 0 ? coll[0] : null;
}
first([]); // null
first([3, 2]); // 3
first(['code-basics', 'hexlet']); // code-basics
Дженерики большая тема, которая раскрывается в следующих уроках. Сейчас наша задача познакомиться с общей концепцией и постепенно начать ее использовать.
Реализуйте дженерик last()
, который извлекает последний элемент из массива если он есть или null — если его нет:
last([]); // null
last([3, 2]); // 2
last(['code-basics', 'hexlet']); // hexlet
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1import * as ta from 'type-assertions';
2
3import last from './index';
4
5test('function', () => {
6 expect(last([])).toBe(null);
7 expect(last([3, 4])).toBe(4);
8 expect(last(['cat', 'dog'])).toBe('dog');
9
10 ta.assert<ta.Equal<ReturnType<typeof last<number>>, number | null>>();
11});
12
Решение учителя откроется через: