Type-aware правила ESLint — это правила, которые используют информацию о типах, предоставляемую TypeScript. В отличие от обычных синтаксических проверок, такие правила анализируют не только структуру исходного кода, но и реальные типы переменных, параметров, возвращаемых значений, обобщений и импортируемых сущностей.
Для получения сведений о типах ESLint взаимодействует с компилятором TypeScript через объект Program. Именно эта интеграция позволяет обнаруживать ошибки, которые невозможно выявить на уровне обычного синтаксического анализа.
Примеры type-aware правил из пакета
@typescript-eslint/eslint-plugin:
no-floating-promisesawait-thenableno-misused-promisesrestrict-plus-operandsrestrict-template-expressionsno-unnecessary-type-assertionprefer-nullish-coalescingunbound-methodПодобные проверки значительно повышают качество анализа, однако за дополнительные возможности приходится платить увеличением времени выполнения линтера.
Обычный ESLint анализирует файл изолированно:
При использовании type-aware правил процесс усложняется:
С точки зрения затрат ресурсов происходит переход от анализа отдельного файла к анализу значительной части проекта.
Ключевым фактором производительности становится создание объекта Program.
Упрощённо процесс выглядит следующим образом:
ESLint
↓
typescript-eslint parser
↓
tsconfig.json
↓
TypeScript Program
↓
Type Checker
↓
Type-aware rules
При создании Program TypeScript:
На крупных проектах именно этот этап обычно занимает большую часть времени выполнения линтера.
Type-aware анализ требует понимания происхождения каждого типа.
Рассмотрим пример:
import { User } from './models/user';
function process(user: User) {
return user.name;
}
Чтобы определить тип user.name, TypeScript должен:
models/user.ts.User.Если таких импортов тысячи, объём работы возрастает многократно.
Особенно затратными оказываются:
export *);Стоимость type-aware анализа растёт не линейно относительно количества файлов.
Небольшой проект:
50 файлов
↓
Program создаётся быстро
↓
Проверка занимает секунды
Средний проект:
500 файлов
↓
Большой граф зависимостей
↓
Проверка занимает десятки секунд
Крупный монорепозиторий:
5000+ файлов
↓
Несколько tsconfig
↓
Минуты выполнения
Фактическое время зависит от архитектуры проекта, однако зависимость между размером кодовой базы и стоимостью типизированного анализа всегда заметна.
Не все type-aware правила одинаково нагружают систему.
Относительно лёгкие:
await-thenable
no-unnecessary-type-assertion
Средней сложности:
restrict-template-expressions
prefer-nullish-coalescing
Более тяжёлые:
no-misused-promises
no-floating-promises
unbound-method
Причина заключается в количестве обращений к Type Checker и глубине анализа, необходимой для принятия решения.
Например, правило no-floating-promises должно
определить:
await;.then();Для этого требуется серия запросов к системе типов.
Каждое type-aware правило регулярно запрашивает информацию о типах.
Пример концептуального алгоритма:
const type = checker.getTypeAtLocation(node);
Один такой вызов может инициировать:
Если правило выполняет тысячи подобных запросов, нагрузка возрастает очень быстро.
Скорость работы зависит не только от количества файлов, но и от сложности типовой системы проекта.
Простой тип:
type User = {
id: number;
name: string;
};
Сложный тип:
type Deep<T> =
T extends object
? {
[K in keyof T]: Deep<T[K]>;
}
: T;
Ещё более тяжёлые конструкции:
type Result<T> =
T extends Promise<infer U>
? U
: T extends Array<infer V>
? V
: never;
Особенно дорогими считаются:
Каждый запрос к таким структурам требует дополнительной работы компилятора.
Type-aware анализ увеличивает не только время выполнения, но и объём используемой памяти.
Причины:
Типичная картина:
| Режим | Память |
|---|---|
| Обычный ESLint | десятки мегабайт |
| Type-aware ESLint | сотни мегабайт |
| Крупный монорепозиторий | гигабайты |
На CI-серверах это может становиться серьёзным ограничением.
Наиболее заметная проблема возникает при локальной разработке.
Разработчик ожидает быстрый цикл:
Изменение файла
↓
Сохранение
↓
Проверка
↓
Результат
Если проверка занимает 20–40 секунд, продуктивность снижается.
Особенно чувствительны:
Поэтому во многих командах часть type-aware правил запускается только в CI.
Исторически типизированный анализ строился через настройку:
parserOptions: {
project: './tsconfig.json'
}
При таком подходе создавался полноценный Program на основе указанного конфигурационного файла.
Современные версии typescript-eslint поддерживают
механизм Project Service, который ближе к работе языкового сервиса
TypeScript.
Пример настройки:
parserOptions: {
projectService: true
}
Преимущества:
На больших проектах разница в производительности может быть весьма заметной.
ESLint поддерживает файловый кэш.
Запуск:
eslint . --cache
Механизм работы:
Первый запуск
↓
Полная проверка
↓
Создание кэша
Следующий запуск
↓
Проверяются только изменённые файлы
Для проектов с type-aware правилами использование кэша практически обязательно, поскольку позволяет существенно сократить время повторных запусков.
Одним из самых эффективных способов ускорения работы является уменьшение набора файлов, входящих в Program.
Плохой вариант:
{
"include": ["**/*"]
}
Лучший вариант:
{
"include": ["src/**/*.ts"]
}
Желательно исключать:
Пример:
{
"exclude": [
"dist",
"coverage",
"generated",
"node_modules"
]
}
Чем меньше файлов участвует в построении Program, тем быстрее работает линтер.
Распространённая практика — создание специального конфигурационного файла.
Пример:
tsconfig.json
tsconfig.eslint.json
tsconfig.eslint.json:
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts"
]
}
Такой подход позволяет:
Во многих проектах используется двухуровневая стратегия.
Быстрые правила:
eslint src
Запускаются:
Тяжёлые type-aware правила:
npm run lint:types
Запускаются:
Подобное разделение позволяет сохранить высокий уровень контроля качества без ухудшения пользовательского опыта разработчиков.
В монорепозиториях стоимость type-aware анализа возрастает особенно заметно.
Типичная структура:
packages/
├─ api
├─ frontend
├─ shared
├─ ui
└─ tooling
При неправильной конфигурации один Program может включать тысячи файлов из всех пакетов одновременно.
Последствия:
Поэтому часто применяются:
Для выявления медленных правил используется специальный режим.
Пример:
TIMING=1 eslint src
Результат показывает время выполнения каждого правила.
Типичный вывод:
Rule Time
no-floating-promises 1800ms
no-misused-promises 1400ms
restrict-template-expressions 900ms
Подобная статистика позволяет определить реальные источники замедления и принять решение о необходимости оптимизации.
Type-aware правила обеспечивают обнаружение целого класса ошибок, недоступных обычному ESLint:
const result = fetchData();
result.map(x => x.id);
Без информации о типах подобный код может выглядеть корректным.
Type Checker способен определить, что result имеет тип
Promise, а не массив, и соответствующее правило сообщит об
ошибке.
Однако каждая дополнительная проверка увеличивает:
Поэтому конфигурация линтера обычно представляет собой компромисс между глубиной анализа и производительностью. Наиболее эффективными оказываются настройки, при которых типизированные проверки используются только там, где их диагностическая ценность действительно оправдывает дополнительные вычислительные затраты.