as и @ts-expect-error: когда вы врёте компилятору

границы≈ 9 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Проверка значения стоит нескольких строк, а as — двух букв, и ошибка исчезает мгновенно. Только это не преобразование и не проверка, а обещание: компилятор принимает его на слово и больше к теме не возвращается. Отвечать за правду с этого момента вам, а расплата приходит не в редакторе, а на исполнении.

Разделы темы:·

У unknown два выхода: доказать проверкой или пообещать через as. Первый разбирала прошлая статья, займёмся вторым — он короче ровно настолько, насколько опаснее.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни одной диагностики. Компилятор не заглянул внутрь объекта — заглядывать ему нечем: он работает с текстом программы, а не со значениями. as — то самое место, где можно сказать «здесь я знаю лучше», и возражений не будет.

Отсюда главное недоразумение. as похож на приведение типа из других языков, но ничего не приводит: в выданном JavaScript от него не остаётся даже пробела.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

typeof promised отвечает «string», хотя компилятор до последнего считал переменную числом. Типы стираются при сборке, а typeof спрашивает у настоящего значения — расходятся они ровно там, где as соврал.

as отвечает на вопрос «как считать этот тип», а не «что сделать со значением». Значения к моменту сборки ещё никто не видел.

Врать можно не что угодно. Компилятор пропускает as, только если типы «достаточно перекрываются» — то есть хотя бы один из них присваивается другому.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В диагностике сразу и диагноз, и рецепт: «Преобразование типа "number" в тип "string" может привести к ошибке, так как ни один из типов не перекрывается с другим в достаточной степени. Если это сделано намеренно, сначала преобразуйте выражение в "unknown"». Через unknown совместимо всё: он принимает любое значение и приводится к любому типу.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Обратите внимание: text.length не падает, а тихо даёт undefined — обращение к несуществующему свойству числа ошибкой не считается. Падение приходит строкой ниже, когда undefined пытаются вызвать.

Так двойное приведение оказывается полезной приметой. Одиночный as компилятор ещё как-то сторожит, as unknown as он не проверяет вовсе. Встретив такую запись — в чужом коде или в своём, — читайте её как отметку «здесь проверок нет и не будет», и требуйте рядом комментарий с основанием.

TS2352 ловит грубое враньё. Настоящие аварии случаются на приведениях, которые правило соседства пропускает без звука, — там, где тип «почти» подходит.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Асимметрия здесь показательная: аннотация const half: Point = { x: 1 } дала бы честную TS2741 про отсутствующее поле. Достаточно заменить двоеточие на as — и та же строка проходит. Проверка не обойдена хитростью, она просто выключена.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни диагностики, ни исключения — просто NaN, который поедет дальше по программе и всплывёт где-нибудь в отчёте. Сравните с настоящим сужением из главы про размеченные объединения: if (shape.kind === "circle") читает значение на исполнении, и внутри ветки тип известен по-честному. as вписывает тот же ответ в тип, минуя вопрос.

Рядом живёт восклицательный знак — то же самое, только узкоспециальное: он вычёркивает из типа null и undefined.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Читается ! как «здесь точно есть», означает «проверять не будем». Разница видна на запуске.

Как получить нужный типСмотрит на значениеЧто будет с неверными данными
asнеттихо неверный результат или падение позже
!нетто же самое, но про null и undefined
typeof, in, instanceofдаветка просто не выполнится
предикат isда, вашим кодомфункция вернёт false

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

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вторая директива — вся суть приёма. @ts-expect-error не просто гасит ошибку, он её требует: строка, на которой ошибки не оказалось, даёт TS2578. Текст к этому коду компилятор печатает по-английски даже с русской локалью — «Unused '@ts-expect-error' directive.»; эта диагностика собирается в обход словаря переводов, так что не удивляйтесь одинокой английской строке среди русских.

У директивы есть немой родственник — @ts-ignore. Он гасит ошибки следующей строки точно так же, но никогда не сообщает, что стал лишним.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

«Поправим позже» — самая долгоживущая фраза в кодовой базе. С @ts-expect-error она хотя бы напомнит о себе: как только сервер починят и типы сойдутся, компилятор укажет на директиву и попросит её убрать. С @ts-ignore строка молчит и через три года — включая тот случай, когда ошибка под ней давно сменилась на другую, вами не задуманную.

Ситуация@ts-expect-error@ts-ignore
Ошибка на следующей строке естьгаситгасит
Ошибки на следующей строке нетTS2578 на самой директивемолчит
Проблему починилинапомнит убрать себяостанется навсегда
Ошибка сменилась на другуюпогасит и еёпогасит и её

Гасится не выражение, а строка. Многострочный литерал ломает приём: ошибка привязана к строке с полем, а директива стоит над первой.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Две диагностики вместо нуля: TS2578 — директива не пригодилась, TS2322 — ошибка осталась непогашенной. Лечится переносом директивы вплотную к нужной строке, прямо внутрь литерала, перед полем age.

Запретить as насовсем нельзя: бывают факты, которые вы знаете, а система типов выразить не умеет. Классический пример — Object.keys.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Стандартная библиотека обещает string[] не по недосмотру. Из-за структурной типизации значение типа { usd: number; eur: number } во время выполнения вполне может оказаться объектом с лишними ключами — и тогда ровно то же приведение соврёт.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тот же приём, тот же повод — и уже неправда. Разница только в том, откуда пришёл объект. Отсюда рабочее правило: as допустим там, где основание записывается одним проверяемым предложением. «Объект создан в этой функции и наружу не выходил» — основание. «Ну там точно есть age» — нет.

Чем заменять, вы уже знаете. На границе программы — unknown и разбор из прошлой статьи. Внутри программы — сужение проверками и предикаты is. Когда нужно зафиксировать форму объекта, не потеряв точный тип, — satisfies из следующей статьи. На оставшиеся проценты остаётся as, и хорошо бы рядом с ним стоял комментарий, объясняющий, почему на этот раз вы правы.

as и ! не защищают, а глушат. Чем короче путь к тишине, тем внимательнее к нему приглядывайтесь.

  • as не преобразует значение и не проверяет его: он переписывает тип в глазах компилятора, а в собранном JavaScript исчезает без следа.
  • Приведение разрешено только между «достаточно перекрывающимися» типами; отказ приходит как TS2352, а обход через as unknown as снимает и эту последнюю проверку.
  • Опаснее всего правдоподобные приведения — неполный объект как полный, объединение как одна из ветвей: диагностик ноль, а на исполнении undefined, NaN или падение.
  • Восклицательный знак — тот же as, только про null и undefined: он тоже ничего не проверяет.
  • @ts-expect-error требует, чтобы ошибка на следующей строке действительно была, и даёт TS2578, когда гасить стало нечего; @ts-ignore в той же ситуации молчит — поэтому по умолчанию берут первый.
  • as оправдан там, где основание записывается одним проверяемым предложением; во всех остальных местах есть unknown с разбором, сужение проверками и satisfies.

1/4У типа Point два обязательных поля: x и y. В коде написано const half = { x: 1 } as Point;, а следом — обращение к half.y. Что произойдёт?

2/4// @ts-expect-error поставили над строкой, в которой никакой ошибки нет. Что скажет компилятор?

3/4const price = 1990;, а следом const text = price as unknown as string;. Что напечатает console.log(text.length)?

4/4Чем @ts-expect-error лучше @ts-ignore в коде, который будет жить годами?

Убрать as с границы

Ответ сервера разбирают и тут же объявляют пользователем. Поля age в нём нет, поэтому программа падает далеко от места, где данные пришли, и с бессмысленным текстом. Перепишите так, чтобы плохой ответ обнаруживался на входе и говорил, какое именно поле не то. Подсказка: unknown и проверки, которые работают и в обычном JavaScript.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Разбор выносим в функцию (raw: unknown) => User. Внутри — обычные проверки; после каждой компилятор сужает тип сам, и ни одного as не требуется.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

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

Директива, которая напомнит о себе

Сервер присылает поле date, а тип обещает sentAt; расхождение заглушено директивой. Сделайте три шага и посмотрите на диагностики после каждого: замените @ts-ignore на @ts-expect-error; почините тип (поле действительно называется date), оставив директиву на месте; уберите то, что окажется лишним.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

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

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

С @ts-ignore второго шага не случилось бы: тип починили, а строка так и стояла бы, готовая проглотить следующую ошибку.

Оправданный as

Цикл по ключам объекта не собирается: TS7053, потому что Object.keys обещает string[], а индексировать rates произвольной строкой нельзя. Почините приведением — и обязательно запишите в комментарии основание, по которому здесь можно соврать. Если основание сформулировать не выходит, значит, врать нельзя.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тип ключей выводим из самого объекта, чтобы он не разъехался при правке, и приводим результат Object.keys к нему.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Основание держится ровно до тех пор, пока объект остаётся своим. Стоит принимать rates параметром — и лишние ключи станут возможны: у вызывающего кода объект мог быть шире, чем его тип. Тогда честнее Object.entries с проверкой значений или перебор по заранее известному списку валют.