as и @ts-expect-error: когда вы врёте компилятору
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Проверка значения стоит нескольких строк, а as — двух букв, и ошибка исчезает мгновенно. Только это не преобразование и не проверка, а обещание: компилятор принимает его на слово и больше к теме не возвращается. Отвечать за правду с этого момента вам, а расплата приходит не в редакторе, а на исполнении.
У unknown два выхода: доказать проверкой или пообещать через as. Первый разбирала прошлая статья, займёмся вторым — он короче ровно настолько, насколько опаснее.
Ни одной диагностики. Компилятор не заглянул внутрь объекта — заглядывать ему нечем: он работает с текстом программы, а не со значениями. as — то самое место, где можно сказать «здесь я знаю лучше», и возражений не будет.
Отсюда главное недоразумение. as похож на приведение типа из других языков, но ничего не приводит: в выданном JavaScript от него не остаётся даже пробела.
typeof promised отвечает «string», хотя компилятор до последнего считал переменную числом. Типы стираются при сборке, а typeof спрашивает у настоящего значения — расходятся они ровно там, где as соврал.
as отвечает на вопрос «как считать этот тип», а не «что сделать со значением». Значения к моменту сборки ещё никто не видел.
Врать можно не что угодно. Компилятор пропускает as, только если типы «достаточно перекрываются» — то есть хотя бы один из них присваивается другому.
В диагностике сразу и диагноз, и рецепт: «Преобразование типа "number" в тип "string" может привести к ошибке, так как ни один из типов не перекрывается с другим в достаточной степени. Если это сделано намеренно, сначала преобразуйте выражение в "unknown"». Через unknown совместимо всё: он принимает любое значение и приводится к любому типу.
Обратите внимание: text.length не падает, а тихо даёт undefined — обращение к несуществующему свойству числа ошибкой не считается. Падение приходит строкой ниже, когда undefined пытаются вызвать.
Так двойное приведение оказывается полезной приметой. Одиночный as компилятор ещё как-то сторожит, as unknown as он не проверяет вовсе. Встретив такую запись — в чужом коде или в своём, — читайте её как отметку «здесь проверок нет и не будет», и требуйте рядом комментарий с основанием.
TS2352 ловит грубое враньё. Настоящие аварии случаются на приведениях, которые правило соседства пропускает без звука, — там, где тип «почти» подходит.
Асимметрия здесь показательная: аннотация const half: Point = { x: 1 } дала бы честную TS2741 про отсутствующее поле. Достаточно заменить двоеточие на as — и та же строка проходит. Проверка не обойдена хитростью, она просто выключена.
Ни диагностики, ни исключения — просто NaN, который поедет дальше по программе и всплывёт где-нибудь в отчёте. Сравните с настоящим сужением из главы про размеченные объединения: if (shape.kind === "circle") читает значение на исполнении, и внутри ветки тип известен по-честному. as вписывает тот же ответ в тип, минуя вопрос.
Рядом живёт восклицательный знак — то же самое, только узкоспециальное: он вычёркивает из типа null и undefined.
Читается ! как «здесь точно есть», означает «проверять не будем». Разница видна на запуске.
| Как получить нужный тип | Смотрит на значение | Что будет с неверными данными |
|---|---|---|
as | нет | тихо неверный результат или падение позже |
! | нет | то же самое, но про null и undefined |
typeof, in, instanceof | да | ветка просто не выполнится |
предикат is | да, вашим кодом | функция вернёт false |
Иногда врать приходится не одному выражению, а целой строке — скажем, вы уверены, что ошибаются чужие типы, а переписывать их нечем. Для этого есть комментарий-директива: она гасит все ошибки следующей строки.
Вторая директива — вся суть приёма. @ts-expect-error не просто гасит ошибку, он её требует: строка, на которой ошибки не оказалось, даёт TS2578. Текст к этому коду компилятор печатает по-английски даже с русской локалью — «Unused '@ts-expect-error' directive.»; эта диагностика собирается в обход словаря переводов, так что не удивляйтесь одинокой английской строке среди русских.
У директивы есть немой родственник — @ts-ignore. Он гасит ошибки следующей строки точно так же, но никогда не сообщает, что стал лишним.
«Поправим позже» — самая долгоживущая фраза в кодовой базе. С @ts-expect-error она хотя бы напомнит о себе: как только сервер починят и типы сойдутся, компилятор укажет на директиву и попросит её убрать. С @ts-ignore строка молчит и через три года — включая тот случай, когда ошибка под ней давно сменилась на другую, вами не задуманную.
| Ситуация | @ts-expect-error | @ts-ignore |
|---|---|---|
| Ошибка на следующей строке есть | гасит | гасит |
| Ошибки на следующей строке нет | TS2578 на самой директиве | молчит |
| Проблему починили | напомнит убрать себя | останется навсегда |
| Ошибка сменилась на другую | погасит и её | погасит и её |
Гасится не выражение, а строка. Многострочный литерал ломает приём: ошибка привязана к строке с полем, а директива стоит над первой.
Две диагностики вместо нуля: TS2578 — директива не пригодилась, TS2322 — ошибка осталась непогашенной. Лечится переносом директивы вплотную к нужной строке, прямо внутрь литерала, перед полем age.
Запретить as насовсем нельзя: бывают факты, которые вы знаете, а система типов выразить не умеет. Классический пример — Object.keys.
Стандартная библиотека обещает string[] не по недосмотру. Из-за структурной типизации значение типа { usd: number; eur: number } во время выполнения вполне может оказаться объектом с лишними ключами — и тогда ровно то же приведение соврёт.
Тот же приём, тот же повод — и уже неправда. Разница только в том, откуда пришёл объект. Отсюда рабочее правило: 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.
Разбор выносим в функцию (raw: unknown) => User. Внутри — обычные проверки; после каждой компилятор сужает тип сам, и ни одного as не требуется.
Строк стало больше, но каждая из них нужна и на исполнении: без них код падал бы и в чистом JavaScript, просто позже и невнятнее. Сравните с исходником — там те же проверки заменяла одна вера в as.
Директива, которая напомнит о себе
Сервер присылает поле date, а тип обещает sentAt; расхождение заглушено директивой. Сделайте три шага и посмотрите на диагностики после каждого: замените @ts-ignore на @ts-expect-error; почините тип (поле действительно называется date), оставив директиву на месте; уберите то, что окажется лишним.
Первый шаг ничего не меняет: ошибка под директивой есть, обе её гасят одинаково. Второй — самый интересный: как только тип совпал с реальностью, гасить стало нечего, и директива сама превратилась в ошибку.
Третий шаг — убрать директиву. Остаётся код, в котором тип описывает то, что приходит на самом деле, и заглушек нет вовсе.
С @ts-ignore второго шага не случилось бы: тип починили, а строка так и стояла бы, готовая проглотить следующую ошибку.
Оправданный as
Цикл по ключам объекта не собирается: TS7053, потому что Object.keys обещает string[], а индексировать rates произвольной строкой нельзя. Почините приведением — и обязательно запишите в комментарии основание, по которому здесь можно соврать. Если основание сформулировать не выходит, значит, врать нельзя.
Тип ключей выводим из самого объекта, чтобы он не разъехался при правке, и приводим результат Object.keys к нему.
Основание держится ровно до тех пор, пока объект остаётся своим. Стоит принимать rates параметром — и лишние ключи станут возможны: у вызывающего кода объект мог быть шире, чем его тип. Тогда честнее Object.entries с проверкой значений или перебор по заранее известному списку валют.