Миграция и внедрение
Внедрение TypeScript в существующий код — постепенный переход файл за файлом, @ts-expect-error против @ts-ignore, включение strictNullChecks на большом проекте, контроль соглашений в CI и ревью пул-реквеста, полного any.
6 вопросов
JuniorТеорияОчень частоКак внедрить TypeScript в большую существующую кодовую базу на JavaScript?
Как внедрить TypeScript в большую существующую кодовую базу на JavaScript?
allowJs позволяет .js и .ts компилироваться бок о бок — переименовывайте файл за файлом: листовые модули первыми, горячие пути последними. checkJs с JSDoc — промежуточная ступень: проверяет ещё не переименованные файлы. Строгие флаги — когда почти весь код .ts.
Типичные ошибки
- ✗Считать, что проект не может смешивать
.jsи.tsв одной сборке - ✗Включать
strictв самом начале, из-за чего миграция тонет в ошибках до первой типизации - ✗Упускать
checkJsвместе с JSDoc как способ типизировать файлы до переименования
Уточняющие вопросы
- →Какие файлы вы переведёте первыми и почему не начать с общего ядра?
- →Что
allowJsменяет в том, что компилятор в итоге эмитит?
JuniorТеорияЧастоВ чём разница между @ts-expect-error и @ts-ignore?
В чём разница между @ts-expect-error и @ts-ignore?
Оба глушат ошибку на следующей строке, но @ts-expect-error требует, чтобы она там была: как только строка перестала падать, сама директива становится ошибкой и её приходится убрать. @ts-ignore не жалуется никогда: переживает исправление и гниёт в слепое пятно.
Типичные ошибки
- ✗Думать, что оба ведут себя одинаково после написания и различаются лишь названием
- ✗Считать
@ts-ignoreотслеживаемым долгом, хотя ничто не заставляет к нему вернуться - ✗Полагать, что комментарий-подавление действует на весь файл, а не только на следующую строку
Уточняющие вопросы
- →Как заставить каждое подавление в кодовой базе нести письменную причину?
- →Почему
@ts-expect-errorсам становится ошибкой, когда код под ним исправлен?
MiddleДизайнЧастоВы ревьюите пул-реквест на 800 строк от коллеги, который новичок в TypeScript. Код работает, тесты проходят. В нём также одиннадцать аннотаций any, четыре комментария @ts-ignore над строками, которые на первый взгляд в порядке, несколько приведений as Config на данных, приходящих из fetch, и одно as unknown as Session. Автор зажат сроком и уже раздражён системой типов. Объясните, как вы будете разбирать эти конструкции — что заблокирует влитие, что вы примете с последующей задачей и что вы измените в кодовой базе или процессе, чтобы следующий пул-реквест этого автора не выглядел так же.
Вы ревьюите пул-реквест на 800 строк от коллеги, который новичок в TypeScript. Код работает, тесты проходят. В нём также одиннадцать аннотаций any, четыре комментария @ts-ignore над строками, которые на первый взгляд в порядке, несколько приведений as Config на данных, приходящих из fetch, и одно as unknown as Session. Автор зажат сроком и уже раздражён системой типов. Объясните, как вы будете разбирать эти конструкции — что заблокирует влитие, что вы примете с последующей задачей и что вы измените в кодовой базе или процессе, чтобы следующий пул-реквест этого автора не выглядел так же.
Судите по радиусу поражения, а не по количеству. as на данных из fetch — непроверяемое утверждение о рантайме: блокируйте, валидируйте на границе. Каждый @ts-ignore — в @ts-expect-error с причиной, умирающий с ошибкой. any на локальной подождёт, на экспорте — нет.
Типичные ошибки
- ✗Принимать приведение
asна внешних данных за типобезопасность, а не за непроверяемое утверждение о рантайме - ✗Взвешивать подавления по количеству, а не по площади поверхности, которую каждое из них открывает
- ✗Принимать
@ts-ignoreтам, где описанный@ts-expect-errorсамоуничтожился бы после исправления ошибки
Уточняющие вопросы
- →Как типизировать границу
fetch, чтобы приведение там не требовалось вовсе? - →Что вы добавите в CI, чтобы следующий пул-реквест не смог это повторить?
MiddleДизайнЧастоКоманда из двенадцати человек продолжает поставлять код, который компилируется, но тихо сдаёт типы: any на границах, @ts-ignore над неудобными строками и объекты, переформованные через as вместо валидации. Сборка остаётся зелёной, поэтому влитию ничто не мешает, и эрозия невидима до момента, когда кто-то ловит падение в рантайме внутри якобы типизированного пути. Спроектируйте контроль в CI, который сделает этот класс эрозии невозможным для незаметного влития, и объясните, как вы внедрите его на кодовой базе, где таких нарушений уже сотни, не останавливая продуктовую работу.
Команда из двенадцати человек продолжает поставлять код, который компилируется, но тихо сдаёт типы: any на границах, @ts-ignore над неудобными строками и объекты, переформованные через as вместо валидации. Сборка остаётся зелёной, поэтому влитию ничто не мешает, и эрозия невидима до момента, когда кто-то ловит падение в рантайме внутри якобы типизированного пути. Спроектируйте контроль в CI, который сделает этот класс эрозии невозможным для незаметного влития, и объясните, как вы внедрите его на кодовой базе, где таких нарушений уже сотни, не останавливая продуктовую работу.
tsc — только пол: всё это компилируется начисто. Эрозия живёт в линтере: no-explicit-any, no-unsafe-* и ban-ts-comment, отвергающий @ts-ignore и разрешающий @ts-expect-error только с причиной. Внедряйте храповиком: базовая линия, падение CI при росте.
Типичные ошибки
- ✗Считать, что
tscилиstrictотвергаетany,asи@ts-ignore— всё это компилируется начисто - ✗Ставить ворота на ноль нарушений с первого дня, что останавливает продуктовую работу вместо разворота тренда
- ✗Подавлять существующие нарушения так, что они прячутся от правила, вместо фиксации базовой линии
Уточняющие вопросы
- →Каким правилам линтера нужна информация о типах и чего это стоит в CI?
- →Как не дать правке базовой линии вверх пройти незамеченной?
MiddleДизайнЧастоВаша команда владеет кодовой базой на TypeScript в 250 тысяч строк, которая с самого начала живёт с strictNullChecks: false. Включение флага в ветке даёт около 4000 ошибок в 600 файлах, а продуктовую работу останавливать нельзя. Нужно прийти к strictNullChecks: true без долгоживущей ветки, без разового большого влития и не позволяя новому коду добавлять свежие нарушения, пока разбирается накопленный долг. Опишите, как вы выстроите последовательность внедрения, как не дадите уже вычищенным файлам деградировать обратно и как будете решать, где настоящая проверка на null — это лечение, а где приемлемо утверждение non-null.
Ваша команда владеет кодовой базой на TypeScript в 250 тысяч строк, которая с самого начала живёт с strictNullChecks: false. Включение флага в ветке даёт около 4000 ошибок в 600 файлах, а продуктовую работу останавливать нельзя. Нужно прийти к strictNullChecks: true без долгоживущей ветки, без разового большого влития и не позволяя новому коду добавлять свежие нарушения, пока разбирается накопленный долг. Опишите, как вы выстроите последовательность внедрения, как не дадите уже вычищенным файлам деградировать обратно и как будете решать, где настоящая проверка на null — это лечение, а где приемлемо утверждение non-null.
Кампания по файлам, а не переключение флага. Растущий строгий tsconfig покрывает новые и вычищенные файлы, храповик валит CI при росте. 4000 подавите и разбирайте обычными пул-реквестами. Проверки — для отсутствующих значений, ! — для верного, но недоказуемого инварианта.
Типичные ошибки
- ✗Считать это переключением флага, а не пофайловой кампанией с длинным долгом за спиной
- ✗Хвататься за
!, чтобы быстро погасить ошибки, что прячет отсутствующие значения вместо их обработки - ✗Полагать, что вычищенный файл не деградирует после включения флага, хотя храповик ничем не обеспечен
Уточняющие вопросы
- →Как не дать уже вычищенному файлу молча деградировать в поздних пул-реквестах?
- →Когда утверждение non-null — честный ответ, а не срезание угла?
SeniorДизайнИногдаСеньор в вашей команде написал хелпер на 60 строк из условных и mapped-типов, который выводит params, body и response обработчика маршрута прямо из строки маршрута. Он корректен и уже поймал два настоящих бага. Он же выдаёт сорокастрочное сообщение об ошибке при малейшем несовпадении, изменить его не может больше никто в команде, а редактор думает около двух секунд, прежде чем предложить автодополнение в использующих его файлах. Теперь этот инженер хочет применить ту же технику к слою данных. Займите позицию по этому размену и объясните, как вы вообще решаете, когда хитрый тип себя окупает, а когда его надо заменить чем-то проще плюс проверкой в рантайме.
Сеньор в вашей команде написал хелпер на 60 строк из условных и mapped-типов, который выводит params, body и response обработчика маршрута прямо из строки маршрута. Он корректен и уже поймал два настоящих бага. Он же выдаёт сорокастрочное сообщение об ошибке при малейшем несовпадении, изменить его не может больше никто в команде, а редактор думает около двух секунд, прежде чем предложить автодополнение в использующих его файлах. Теперь этот инженер хочет применить ту же технику к слою данных. Займите позицию по этому размену и объясните, как вы вообще решаете, когда хитрый тип себя окупает, а когда его надо заменить чем-то проще плюс проверкой в рантайме.
Судите тип по цене для команды — сообщения об ошибках, время компиляции, bus factor — а не по тому, что доказывает. Хитрый тип окупается лишь за устойчивой именованной границей, потребляясь как обычный тип. Где доказательство дорого, скучный тип с проверкой на границе лучше.
Типичные ошибки
- ✗Судить тип по тому, что он доказывает, а не по тому, чего он стоит поддерживающей его команде
- ✗Считать гарантию времени компиляции строго лучшей, чем проверка на границе плюс простой тип
- ✗Запрещать хитрые типы начисто вместо того, чтобы запереть их за устойчивой именованной границей
Уточняющие вопросы
- →Что изменило бы ваш ответ, будь хелпер опубликован как библиотека, а не жил внутри компании?
- →Как измерить, во что этот хелпер реально обходится редактору и сборке сегодня?