Когда мы присваиваем значение или передаем аргументы в вызов функции, проверка типов TypeScript проверяет типы на совместимость. При передаче аргументов в функцию проверка выполняется и для типов параметров, и для возвращаемых типов.
Если мы передадим возвращающую number
функцию для колбека функции-сортировки, которая ожидает возврата -1 | 0 | 1
, то получим ошибку Type 'number' is not assignable to type '0 | 1 | -1'.
:
type ComparatorCallback = (item1: number, item2: number) => -1 | 0 | 1
declare function sort(arr: Array<number>, callback: ComparatorCallback): Array<number>
const arr = [1, 2, 3];
const comparator = (item1: number, item2: number) => Math.sign(item1 - item2);
// (item1: number, item2: number) => number;
sort(arr, comparator); // Error: Type 'number' is not assignable to type '0 | 1 | -1'.
Множество значений из объединения трех литеральных типов -1 | 0 | 1
является подмножеством number
. Но из ошибки можно понять, что возвращаемый тип должен быть либо таким же, либо более узким. Такое поведение проверки типов называется ковариантностью.
Чтобы решить проблему с ComparatorCallback
, нам нужно сузить возвращаемый тип функции comparator
до -1 | 0 | 1
или более узкого. Перепишем код без Math.sign
, чтобы вернуть нужный тип:
type ComparatorCallback = (item1: number, item2: number) => -1 | 0 | 1
declare function sort(arr: Array<number>, callback: ComparatorCallback): Array<number>
const arr = [1, 2, 3];
const comparator = (item1: number, item2: number) => {
// (item1: number, item2: number) => -1 | 0 | 1;
if (item1 === item2) {
return 0;
}
return item1 > item2 ? 1 : -1;
};
sort(arr, comparator);
Теперь код проходит проверку типов. Возвращаемый тип comparator
стал более узким, чем требуемый в ComparatorCallback
.
Для аргументов функции проверка типов выполняется в обратном порядке. Если мы передадим функцию, которая ожидает литеральный тип 1
вместо number
, то получим ошибку Type 'number' is not assignable to type '1'.
:
type ComparatorCallback = (item1: number, item2: number) => -1 | 0 | 1
declare function sort(arr: Array<number>, callback: ComparatorCallback): Array<number>
const arr = [1, 2, 3];
const comparator = (item1: 1, item2: number) => Math.sign(item1 - item2) as -1 | 0 | 1;
sort(arr, comparator); // Type 'number' is not assignable to type '1'.
Тип 1
является подмножеством number
. И в нашем примере мы передаем в функцию sort
функцию, которая ожидает более узкий тип на входе. Также вы можете обратить внимание, что мы приводим тип возвращаемого значения к -1 | 0 | 1
с помощью ключевого слова as
. Нам потребовалось приведение вниз, так как типизация Math.sign
возвращает number
.
Когда мы передаем аргументы в функцию, то ожидаемые типы параметров должны быть более широкими, чем фактические. Такое поведение проверки типов называется контравариантностью.
Попробуйте самостоятельно объяснить поведение проверки типов через вариантность в следующем примере:
type Formatter = (val: string) => string;
const formatToConcrete: Formatter = (): 'test' => 'test';
const formatToNumber: Formatter = (val: '1') => val; // Error!
Если при работе с TypeScript учитывать наследие JavaScript с утиной типизацией, то все становится на свои места.
Чтобы код не упал с ошибкой, достаточно проверки на наличие полей или методов нужных типов. А чтобы получить гарантии во внешнем мире, нужно, чтобы переменная попадала под внешние ограничения. Для этого тип должен быть более узким или таким же.
Реализуйте функцию applyTransactions(wallet)
и типы Transaction
, Wallet
. Wallet
содержит список транзакций в виде массива элементов типа Transaction
и числовой баланс. Transaction
содержит метод apply
, который принимает баланс и возвращает новый баланс.
Функция applyTransactions(wallet)
должна принимать аргумент типа Wallet
и возвращать баланс после применения всего списка транзакций. В случае ошибки в одной из транзакций должно вернуться изначальный баланс, и не продолжать применять транзакции.
const wallet: Wallet = {
transactions: [
{
apply: (amount) => amount + 1,
},
],
balance: 0
}
console.log(applyTransactions(wallet)) // 1
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1import * as ta from 'type-assertions';
2import applyTransactions, { Transaction, Wallet } from './index';
3
4test('applyTransactions', () => {
5 const wallet: Wallet = {
6 balance: 100,
7 transactions: [
8 {
9 apply: (amount: number) => amount + 10,
10 },
11 {
12 apply: (amount: number) => amount - 20,
13 },
14 {
15 apply: (amount: number) => amount + 30,
16 },
17 ],
18 };
19
20 expect(applyTransactions(wallet)).toBe(120);
21 expect(wallet.balance).toBe(100);
22
23 const wallet2: Wallet = {
24 balance: 10,
25 transactions: [
26 {
27 apply: (amount: number) => amount + 10,
28 },
29 {
30 apply: () => {
31 throw new Error('Error');
32 },
33 },
34 {
35 apply: (amount: number) => amount + 30,
36 },
37 ],
38 };
39
40 expect(applyTransactions(wallet2)).toBe(10);
41
42 ta.assert<ta.Equal<Parameters<Transaction['apply']>, [number]>>();
43});
44
Решение учителя откроется через: