Раздел 46 · Effect: Тихая Академия

Как читать «Ук»

middle~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Библиотека Академии больше, чем кажется снаружи. Полки уходят вверх в темноту, между ними пахнет пылью и старой бумагой. За столом у входа сидит большой рыжий орангутан с латунным ключом на шнурке. Это Библиотекарь. Ты кладёшь перед ним свиток башни T-1, небольшой модуль towers.ts: первая башня маршрута, смотритель башни, текст ошибки для журнала. Сундук остаётся у порога и ждёт.

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

Библиотекарь это проверка типов во плоти. Он читает записанное заклинание раньше, чем его произнесли, и показывает пальцем туда, где запись обещает одно, а делает другое. Его «Ук» это ошибка TypeScript: коротко, точно, и непонятно, пока не научишься читать. В этом уроке ты научишься: где смотреть ошибку, в каком порядке её читать, и какие три флага компилятора делают «Ук» строже, а свитки честнее.

Где живёт «Ук»

Одну и ту же ошибку TypeScript показывает в трёх местах.

В редакторе. Красная волнистая линия под местом, где не сошлось. Наведи на неё курсор, и всплывёт текст ошибки. Наведи курсор на любое имя без ошибки, и редактор покажет его тип. Это самый быстрый способ узнать, что TypeScript думает о твоём коде:

const route = ['T-1', 'T-2', 'T-3'];
const next = route[0];

Наведи на next, и редактор покажет const next: string | undefined. Ты ожидал string, а компилятор считает иначе. Почему, разберём ниже. Пока запомни привычку: прежде чем спорить с ошибкой, наведи курсор и посмотри, какой тип видит компилятор.

В терминале. Команда tsc --noEmit проверяет весь проект и печатает ошибки строками вида файл(строка,столбец): error TSкод: текст. Так же проверку гоняют в CI.

В задачах курса. У задач с шагом Т код сначала проходит проверку типов вместе со скрытыми тестами, и только потом тесты запускаются. Если типы не сошлись, панель прогона покажет «Ук» и список ошибок в виде файл:строка:столбец TSкод: текст. Формат чуть другой, чем у tsc, а содержание то же.

У каждой ошибки есть код, например TS2322. Код не надо запоминать, но по нему легко искать: одна и та же ошибка в чужом вопросе и в твоём коде будет с одним кодом.

Длинная ошибка читается снизу вверх

Ошибка TypeScript часто занимает несколько строк, и каждая следующая сдвинута вправо. Это не повтор одного и того же разными словами, а спуск вглубь:

  • первая строка говорит, что не сошлось целиком: вот это значение не подходит вот этому типу;
  • каждая следующая спускается на уровень глубже: в каком поле, в каком элементе, в каком варианте объединения;
  • последняя строка это самое маленькое несовпадение, до которого компилятор докопался.

Поэтому читай снизу. Последняя строка говорит, что не сошлось, а строки выше говорят, где это лежит. Первая строка длинная и пугает, но её лучше читать последней: когда уже знаешь, что ищешь, она становится адресом.

Перевод с Ук

Вот что сказал бы шаг Т про свиток, который ты принёс. Это настоящий вывод, со стартера задачи из разминки:

Ук! Типы не сходятся:
src/towers.ts:5:60 TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.
src/towers.ts:8:81 TS2375: Type '{ keeper: string | undefined; code: string; }' is not assignable to type 'Tower' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
  Types of property 'keeper' are incompatible.
    Type 'string | undefined' is not assignable to type 'string'.
      Type 'undefined' is not assignable to type 'string'.
src/towers.ts:14:51 TS18046: 'error' is of type 'unknown'.
test/towers.test.ts:11:47 TS2344: Type 'string | undefined' does not satisfy the constraint '"Expected: string, Actual: never" | "Expected: undefined, Actual: never"'.
  Type 'undefined' is not assignable to type '"Expected: string, Actual: never" | "Expected: undefined, Actual: never"'.

Возьмём ошибку про восьмую строку и пойдём снизу.

Type 'undefined' is not assignable to type 'string'. Самое маленькое несовпадение: куда-то, где разрешена только строка, попадает undefined.

Type 'string | undefined' is not assignable to type 'string'. Уровнем выше видно, что именно попадает: значение типа string | undefined. В свитке такой тип у параметра keeper: в него можно передать имя смотрителя, а можно undefined.

Types of property 'keeper' are incompatible. Ещё выше адрес: всё это происходит в поле keeper.

Первая строка теперь читается как предложение. Объект { keeper: string | undefined; code: string; }, который возвращает функция, не подходит под тип Tower, потому что включён флаг exactOptionalPropertyTypes. Компилятор даже советует добавить undefined к типам полей Tower. Совет честный, но это совет поменять обещание, а не код. Обещание здесь правильное, и менять его нельзя.

Адрес src/towers.ts:8:81: восьмая строка, восемьдесят первый столбец. Там начинается объект { ...tower, keeper }, который возвращает стрелка.

Итого: withKeeper обещает вернуть Tower, где смотрителя либо нет совсем, либо он строка. А возвращает объект, в котором поле keeper есть всегда, и иногда в нём лежит undefined. Это разные объекты: у второго 'keeper' in tower даёт true, а Object.keys перечисляет keeper. Код, который проверяет, есть ли у башни смотритель, ошибётся.

Остальные три ошибки короче, их переведёшь сам. Подсказки: первая про доступ по индексу и флаг noUncheckedIndexedAccess, третья про unknown в reason: этот тип ты встречал во втором уроке. А четвёртая ошибка вообще не в твоём файле, а в скрытом тесте. Тест проверяет тип: expectTypeOf(first).returns.toEqualTypeOf<string | undefined>(). Странные строки "Expected: string, Actual: never" это то, как библиотека expect-type пишет несовпадения прямо в типах. Ошибка в тесте здесь эхо первой ошибки: когда first начнёт говорить правду, исчезнет и она. Отсюда правило: сначала чини ошибки в своём коде сверху вниз, ошибки в тестах смотри последними.

Три строгих флага

Строгость «Ук» зависит от настроек компилятора. Для этого курса включены три флага, и все задачи проверяются с ними.

strict

strict это не одна проверка, а семейство. Среди них:

  • strictNullChecks: null и undefined это отдельные типы, и string их не включает;
  • noImplicitAny: параметр без типа, который нельзя вывести, это ошибка, а не молчаливый any;
  • useUnknownInCatchVariables: в catch у ошибки тип unknown, это ты видел во втором уроке.

С TypeScript 6.0 strict включён по умолчанию. Но проект, который раньше жил без него, после перехода на 6.0 выключает его в tsconfig.json явно. Тогда из трёх ошибок свитка останется одна, про unknown, а exactOptionalPropertyTypes компилятор без strictNullChecks даже не даст включить: ответит ошибкой TS5052.

noUncheckedIndexedAccess

Этот флаг в strict не входит. Без него элемент массива по индексу имеет тип элемента, сколько бы элементов ни было:

const route = ['T-1', 'T-2', 'T-3'];
const next: string = route[3];
console.log(next.toUpperCase());

Без флага код собирается и падает при запуске:

TypeError: Cannot read properties of undefined (reading 'toUpperCase')

С флагом route[3] получает тип string | undefined, и компилятор останавливает тебя раньше:

route.ts(2,7): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.

То же с объектами-словарями: в Record<string, Tower> по произвольному ключу лежит Tower | undefined. Цена флага: после доступа по индексу нужна проверка. Цикл for (const tower of route) флаг не задевает: там каждый элемент существует, и тип у него string.

exactOptionalPropertyTypes

Этот флаг тоже не входит в strict. Он различает два объекта, которые без него для TypeScript одинаковы: «поля нет» и «поле есть, в нём undefined».

type Frame = { readonly text: string; readonly route?: string };
const frame: Frame = { text: '42', route: undefined };

Без флага код собирается, и у кадра есть поле route: 'route' in frame даёт true, Object.keys(frame) возвращает ['text', 'route']. С флагом route?: string значит ровно «поля может не быть», а класть туда undefined нельзя:

frame.ts(2,7): error TS2375: Type '{ text: string; route: undefined; }' is not assignable to type 'Frame' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
  Types of property 'route' are incompatible.
    Type 'undefined' is not assignable to type 'string'.

Если undefined в поле действительно допустим, это пишут явно: route?: string | undefined. Тогда тип честно говорит, что бывает три случая, а не два.

Где тут дыра

Ринцвальд заглядывает в Библиотеку, видит Библиотекаря с поднятым пальцем и быстро что-то правит в своём свитке.

export const first = (route: readonly string[]): string => route[0]!;

Код собирается. На маршруте Ринцвальда ['T-1', 'T-2'] функция отдаёт 'T-1', и first(...).toUpperCase() работает. Возьми пустой маршрут: first([]) возвращает undefined под видом строки, и first([]).toUpperCase() падает с той же TypeError: Cannot read properties of undefined (reading 'toUpperCase'), только уже при запуске, далеко от места, где соврали.

Восклицательный знак после выражения это утверждение non-null. Оно, as и @ts-ignore делают одно и то же: говорят компилятору «я знаю лучше». Иногда ты правда знаешь лучше. Но тогда ты берёшь проверку на себя, и компилятор больше не прикроет. Ринцвальд не убрал дыру. Он убрал палец, который на неё показывал.

Ринцвальд уходит очень быстро.

Разминка

Почини свиток башни: три ошибки из перевода выше. Типы и сигнатуры менять нельзя, кроме возвращаемого типа first, а any, as и @ts-ignore запрещены: заглушить «Ук» не значит его понять. Шаг Т проверит типы, тесты проверят поведение.

Практика

Глубже в архиве

Откуда в TypeScript дыры, которые закрывают строгие флаги, и какой tsconfig.json брать за основу, разбирает урок «Мост и странности TypeScript»: разделы про источники несостоятельности и минимальный tsconfig.

Дальше

Ты чинишь свиток и приносишь его снова. Библиотекарь читает, кивает и молча кладёт перед тобой карточку каталога. На карточке одно слово.

В следующем уроке начинается Библиотека: типы как множества и первая карточка протокола.