open atlas
↑ К треку
Система типов TypeScript вглубь TS · 05 · 05

Лимиты и производительность типов

У умного кода на уровне типов есть реальная цена компиляции — лимиты глубины и количества инстанцирований, взрыв декартова произведения объединений, лаг редактора — и senior умеет её измерять и знает, когда остановиться и валидировать в рантайме.

TS Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Умный инженер выкатывает «полностью типизированный» построитель SQL-запросов. Типы прекрасны — имена колонок, join’ы и формы возврата выводятся сами. Через три спринта вся команда заводит тикеты: автодополнение тянется четыре секунды, редактор пригвождает ядро CPU, а шаг tsc в CI вырос с 40 секунд до девяти минут. Ничего не «сломано» — каждый тип корректен. Цена невидима, потому что живёт целиком на этапе компиляции, и её платит каждый разработчик на каждом нажатии клавиши. Навык senior — не написать умный тип. Это знать его цену, измерять её через --generateTrace и решать, когда правильный ход — удалить тип и валидировать в рантайме.

Два жёстких лимита

Компилятор навязывает два потолка, дающих одно и то же сообщение об ошибке:

  • Глубина инстанцирования — насколько глубоко может вкладываться одна цепочка инстанцирований типов. Не-хвостовая рекурсия упирается около 50; хвостово-рекурсивные условные типы (TS 4.5+) достигают примерно 1000.
  • Количество инстанцирований — глобальный бюджет на то, сколько инстанцирований типов может запустить одно выражение. Компилятор прерывает разрастающийся тип с Type instantiation is excessively deep and possibly infinite., защищаясь от типов, которые иначе зависнут проверку навсегда.

Сообщение не различает «действительно бесконечный» и «конечный, но слишком дорогой». Оба всплывают одинаково, поэтому диагностика — это сначала проверить базовый случай, затем глубину, затем общее число инстанцирований.

Настоящий убийца: декартовы произведения объединений

Глубина редко то, что кусает команды в проде. Катастрофическая цена — комбинаторный взрыв объединений. Когда два объединения встречаются так, что компилятор вынужден материализовать каждую пару, работа — это произведение их размеров:

type Pair<A, B> = A extends any ? (B extends any ? [A, B] : never) : never;
type U = "a" | "b" | "c" | "d";          // 4 члена
type Cross = Pair<U, U>;                  // 4 x 4 = 16 членов
// сцепите три таких и получите 4^3 = 64; десять объединений по 10 -> 10^10 инстанцирований

Дистрибутивное условие над 50-членным объединением, вложенное в другое дистрибутивное условие над 50-членным объединением, — это 2 500 инстанцирований для одного псевдонима типа. Сложите несколько таких — и дойдёте до миллионов. Вот почему невинно выглядящий тип может в 10 раз увеличить лаг редактора всем.

Измерять, а не гадать

Никогда не оптимизируйте типы на ощупь. Компилятор выдаёт точные числа:

# Куда уходит время? Итоги: parse, bind, check, счётчики инстанцирований.
tsc --noEmit --extendedDiagnostics

# Снять профиль, открываемый в chrome://tracing или анализатором.
tsc --noEmit --generateTrace ./trace
npx @typescript/analyze-trace ./trace   # ранжирует самые горячие типы и файлы

# Не дать компилятору прятать виновный тип за "..." в ошибках.
tsc --noEmit --noErrorTruncation

--extendedDiagnostics показывает Instantiations: и Check time — скачок с 200k до 5M инстанцирований после PR указывает прямо на виновника. --generateTrace плюс @typescript/analyze-trace называет конкретный псевдоним типа и место в исходнике, жгущее больше всего времени. --noErrorTruncation раскрывает полный (часто гигантский) тип, на котором компилятор давится, вместо бесполезного ....

Почему это работает

Почему интерфейсы часто проверяются быстрее больших пересечений типов? Объявление interface получает имя, кэшируется и сравнивается по этому имени (и по структуре лениво), поэтому повторные ссылки переиспользуют работу. Большое анонимное пересечение A & B & C & ... заново элаборируется, а его свойства сливаются в каждом месте использования, без имени для кэширования. Для горячей, часто упоминаемой формы объявление её интерфейсом (или именование промежуточного псевдонима, чтобы компилятор мемоизировал) может существенно срезать время проверки. Performance-вики TypeScript от Microsoft рекомендует предпочитать интерфейсы и базовые типы большим пересечениям именно по этой причине.

Митигации, по убыванию рычага

  1. Сократите входы. Избегайте дистрибуции по большим объединениям; отключайте обёрткой [T] там, где не нужно по-членное поведение.
  2. Кэшируйте промежуточное. Дайте рекурсивному или составному типу именованный псевдоним, чтобы компилятор мемоизировал его, а не пересчитывал инлайн на каждой ссылке.
  3. Предпочитайте интерфейсы большим пересечениям для горячих форм; они кэшируются по имени.
  4. Избегайте глубоких цепочек условий. Уплощайте длинные лестницы A extends X ? ... : B extends Y ? ...; они множат число инстанцирований.
  5. Включите --incremental (и project references), чтобы неизменные файлы переиспользовали прежний .tsbuildinfo, и перепроверялся только затронутый граф.
  6. Остановитесь и валидируйте в рантайме. Ход senior: если тип стоит команде больше, чем экономит, удалите его и проверяйте инвариант в рантайме (валидатор схемы, парсер, рантайм-гард). 200-строчный тип, доказывающий, что строка — валидный маршрут, стоит меньше 5-строчной рантайм-проверки, делающей то же и ничего не стоящей редактору.

Вместе эти шесть рычагов движутся от самого дешёвого фикса (сократить входы, без структурных изменений) до самого решительного (удалить тип полностью). Если шаг 1 не помог — скорее всего, перед вами структурная проблема, которую решат только шаги 4–6.

Викторина

PR добавляет дистрибутивное условие над 60-членным объединением, вложенное в другое дистрибутивное условие над 40-членным объединением. Время tsc в CI утраивается. Какова доминирующая цена и какое измерение запустить первым?

Расставь шаги по порядку

Расставьте шаги диагностики и устранения внезапного замедления tsc после тяжёлого по типам PR.

  1. 1 Запустить tsc --noEmit --extendedDiagnostics и сравнить Instantiations / Check time с базовой линией
  2. 2 Запустить tsc --noEmit --generateTrace ./trace, чтобы снять профиль
  3. 3 Запустить @typescript/analyze-trace ./trace, чтобы ранжировать самые горячие типы и места в исходнике
  4. 4 Осмотреть названный тип с --noErrorTruncation, чтобы увидеть его полное разворачивание
  5. 5 Применить митигацию (сузить объединения, закэшировать псевдоним или удалить тип и валидировать в рантайме)
Вспомните перед уходом
  1. 01
    Какие два разных лимита компилятора стоят за 'Type instantiation is excessively deep and possibly infinite.' и как понять, в какой вы упёрлись?
  2. 02
    Почему декартовы произведения объединений — доминирующий риск производительности в коде на уровне типов и как их сдерживать?
  3. 03
    Назовите инструменты измерения и лестницу митигаций и сформулируйте правило, когда вовсе отказаться от типа.
Итог

Этот юнит строил программы на уровне типов; этот урок — отрезвление по их цене. Два жёстких лимита — глубина инстанцирования (~50 не-хвост, ~1000 хвост) и глобальный бюджет числа инстанцирований — оба сообщают «Type instantiation is excessively deep and possibly infinite.», а настоящий продакшен-убийца — декартовы произведения объединений, чья цена это произведение размеров. Измеряйте, а не гадайте, через --extendedDiagnostics, --generateTrace плюс @typescript/analyze-trace и --noErrorTruncation; затем поднимайтесь по лестнице митигаций — сузить объединения, кэшировать псевдонимы, предпочесть интерфейсы, уплощать условия, включить --incremental — и, когда тип стоит команде больше, чем экономит, валидируйте в рантайме. Теперь, когда после тяжёлого по типам PR автодополнение начнёт тормозить, вы тянетесь к --generateTrace первым — а не к рерайту.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки
встречается в1

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.