Коротко: Пошаговое руководство по миграции JavaScript-проекта на TypeScript и внедрению strict: true без остановки релизов и деградации кодовой базы.
Попытка перевести крупный коммерческий JavaScript-проект на TypeScript одним махом с включением флага strict: true почти всегда приводит к блокировке репозитория, сотням или тысячам ошибок компилятора и выгоранию команды. В активном enterprise-приложении невозможно остановить разработку фичей ради глобального рефакторинга.
Единственный надежный подход к переходу на статическую типизацию — инкрементальная миграция. TypeScript изначально спроектирован как надмножество JavaScript: валидный JS-код синтаксически корректен для компилятора TS. Это позволяет организовать параллельное сосуществование двух языков в рамках одного репозитория, плавно конвертировать кодовую базу модуль за модулем и последовательно выходить на максимальный уровень строгой проверки типов.
Шаг 0. Подготовка окружения и гибридный режим (allowJs)
Перед тем как переименовать первый файл, необходимо подготовить инфраструктуру, чтобы добавление TypeScript не сломало существующие пайплайны сборки и поставки.
Базовая конфигурация tsconfig.json для плавного старта
Первоначальный конфигурационный файл должен быть максимально мягким. Главная цель на старте — научить компилятор обрабатывать проект без генерации блокирующих ошибок в существующем легаси-коде.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"jsx": "react-jsx",
"allowJs": true,
"checkJs": false,
"noEmit": true,
"strict": false,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "build"]
}
allowJs: true— разрешает компилятору обрабатывать файлы.jsи.jsxнаряду с.tsи.tsx. Это фундамент для гибридной кодовой базы.checkJs: false— отключает проверку типов внутри JavaScript-файлов через комментарии JSDoc, предотвращая шумные предупреждения в нетронутых модулях.noEmit: true— оставляет компиляторуtscтолько задачу проверки типов (type checking), тогда как транспиляцией занимаются Vite, Webpack (через Babel или SWC) или esbuild.
Настройка сборщиков и изоляция проверки типов
Современные бандлеры быстро удаляют синтаксис типов из .ts-файлов, не выполняя их полную валидацию при сборке.
Чтобы гарантировать корректность типов в проекте, проверку выносят в отдельный процесс:
- В локальном dev-окружении для Vite используют
vite-plugin-checker, а для Webpack —fork-ts-checker-webpack-plugin. - В
package.jsonдобавляют отдельные скрипты валидации:
{
"scripts": {
"type-check": "tsc --noEmit",
"type-check:watch": "tsc --noEmit --watch"
}
}
Анатомия флага strict: true: что спрятано под капотом
Флаг strict: true не является изолированной проверкой. Это мета-параметр, активирующий семейство строгих правил компилятора.
Ключевые проверки strict-семейства
noImplicitAny— запрещает компилятору неявно выводить типanyдля переменных и параметров функций, когда тип не удается вывести из контекста.strictNullChecks— исключаетnullиundefinedиз области допустимых значений всех базовых типов. Без этого флага переменная типаstringможет содержатьnull, что провоцирует рантайм-ошибки вида «Cannot read properties of undefined».strictFunctionTypes— включает контравариантную проверку параметров функций, защищая от некорректной передачи колбэков.strictBindCallApply— гарантирует проверку типов аргументов при вызове методов.bind(),.call()и.apply().strictPropertyInitialization— требует обязательной инициализации свойств классов в теле объявления или в конструкторе.noImplicitThis— вызывает ошибку, если выражениеthisимеет неявный типany.useUnknownInCatchVariables— типизирует переменную ошибки в блокеcatch (err)какunknownвместоany, принуждая к явной проверке типа перед обращением к свойствам.alwaysStrict— принудительно генерирует JS-код с директивой"use strict".
Что strict: true оставляет без внимания
Даже при активном strict: true компилятор по умолчанию пропускает некоторые потенциально опасные конструкции. Для глубокой защиты в production-проектах отдельно настраивают дополнительные флаги:
noUncheckedIndexedAccess— автоматически добавляетundefinedк результату обращения по индексу массива или строковому ключу словаря (Record<string, T>).exactOptionalPropertyTypes— запрещает передавать явныйundefinedв опциональное поле{ prop?: string }, требуя, чтобы свойство было либо строкой, либо вовсе отсутствовало в объекте.noImplicitOverride— требует явного указания ключевого словаoverrideпри переопределении методов родительских классов.noPropertyAccessFromIndexSignature— заставляет использовать синтаксисobj['key']вместоobj.keyдля полей с динамическими сигнатурами индексов.
Стратегия «снизу вверх»: архитектурный порядок конвертации файлов
Попытка начать миграцию с центральных файлов (App.tsx или index.ts) создает каскад нетипизированных зависимостей. Надежнее применять стратегию «от листьев к корню» (leaf-first approach).
Схема зависимостей при миграции:
[Точки входа: main.tsx, routing] (Фаза 4)
│
[Контейнеры, Стейт: Redux / Zustand] (Фаза 3)
│
[UI-компоненты: Buttons, Modals, Forms] (Фаза 3)
│
[API-клиенты, DTO, Модели данных] (Фаза 2)
│
[Чистые утилиты, Хелперы, Форматтеры] (Фаза 1)
Фаза 1. Листовые модули, хелперы и чистые утилиты
Файлы без внутренних зависимостей в проекте: форматирование дат, математические расчеты, валидаторы форм, парсеры строк. Они типизируются быстрее всего и создают надежный фундамент для вышележащих слоев.
Фаза 2. Модели данных, DTO и API-клиенты
Описание контрактов взаимодействия с бэкендом: интерфейсы запросов, DTO-ответы, типизация оберток над HTTP-клиентами.
// src/api/types/user.ts
export interface UserDTO {
id: string;
email: string;
role: 'admin' | 'manager' | 'client';
profile: {
firstName: string;
lastName: string;
avatarUrl?: string;
};
}
Фаза 3. UI-компоненты и слой состояния
Переименование .jsx в .tsx. Описание интерфейсов пропсов, дженериков хуков (useState, useRef, useCallback) и срезов глобального состояния (Redux Toolkit, Zustand).
// src/components/Badge/Badge.tsx
interface BadgeProps {
label: string;
variant?: 'success' | 'warning' | 'danger';
count?: number;
}
export const Badge = ({ label, variant = 'success', count }: BadgeProps) => {
return (
<span className={`badge badge-${variant}`}>
{label} {typeof count === 'number' && `(${count})`}
</span>
);
};
Фаза 4. Роутинг, точки входа и инфраструктурные конфиги
Сборка приложения воедино: конфигурации клиентских роутеров, контекстные провайдеры, корневой файл монтирования DOM (index.tsx/main.tsx).
Тактика постепенного «затягивания гаек» компилятора
Вместо одномоментного включения глобального strict: true целесообразно последовательно активировать флаги strict-семейства.
Поочередное включение проверок
noImplicitAny: true— первоочередной шаг. Устраняет слепые зоны, требуя явной типизации параметров функций и модульных экспортов.strictBindCallApplyиnoImplicitThis— отлавливают проблемы с передачей контекста исполнения функций.useUnknownInCatchVariables: true— переводит обработку ошибок на безопасные рельсы с обязательной проверкой типов:
try {
await fetchUserData(userId);
} catch (error) {
if (error instanceof AxiosError) {
showNotification(error.response?.data?.message ?? 'Сетевая ошибка');
} else if (error instanceof Error) {
showNotification(error.message);
} else {
showNotification('Непредвиденный сбой');
}
}
strictNullChecks: true— наиболее масштабный этап, требующий явного сужения типов (type narrowing) и безопасной работы с опциональными цепочками (?.).strict: true— итоговая фиксация strict-режима, когда все дочерние флаги уже активированы и проект компилируется чисто.
Работа с типами в переходный период: как не превратить TS в any-script
В условиях сжатых сроков часто возникает искушение обойти ошибки типизации через any или директивы подавления. Это сводит на нет практическую ценность миграции.
Разница между any и unknown
Тип any полностью отключает валидацию компилятором и каскадно распространяется по кодовой базе.
Тип unknown гарантирует типобезопасность: компилятор запрещает вызовы методов и доступ к полям объекта до тех пор, пока тип не будет явно проверен тайпгардом.
// Небезопасно: компилятор пропустит опечатку
const badParse = (raw: string): any => JSON.parse(raw);
const user1 = badParse('{}');
console.log(user1.prfile.name); // Ошибка в рантайме
// Безопасно: компилятор принуждает к проверке структуры
const safeParse = (raw: string): unknown => JSON.parse(raw);
const user2 = safeParse('{}');
function isUser(obj: unknown): obj is { profile: { name: string } } {
return (
typeof obj === 'object' &&
obj !== null &&
'profile' in obj &&
typeof (obj as any).profile?.name === 'string'
);
}
if (isUser(user2)) {
console.log(user2.profile.name); // Безопасный доступ
}
Легитимное применение @ts-expect-error вместо @ts-ignore
Директива // @ts-ignore навсегда глушит ошибку на следующей строке, даже если нижележащий код позже будет исправлен.
Директива // @ts-expect-error ожидает обязательную ошибку компиляции. Как только код исправлен и типизирован, компилятор сообщит: «Unused '@ts-expect-error' directive». Это обязывает удалить комментарий и сохраняет чистоту кодовой базы.
// Временная мера до обновления интерфейса внешнего сервиса
// @ts-expect-error: Устаревший API возвращает несовместимый тип данных
const result: CustomReport = legacyExportEngine.generate();
Сторонние библиотеки без @types
Если используемый в проекте пакет не содержит встроенных типов и отсутствует в каталоге DefinitelyTyped (@types/*), достаточно объявить модуль в файле деклараций (например, src/types/vendor.d.ts):
declare module 'legacy-canvas-chart' {
export interface ChartOptions {
width: number;
height: number;
theme?: string;
}
export class ChartEngine {
constructor(container: HTMLElement, options: ChartOptions);
render(data: unknown[]): void;
destroy(): void;
}
}
Защита периметра: автоматизация контроля в CI/CD
Чтобы предотвратить появление нетипизированного кода при параллельной разработке фичей, контроль качества автоматизируют в пайплайнах.
Pre-commit хуки и линтинг
С помощью husky и lint-staged настраивают запуск линтера с набором правил @typescript-eslint:
// .lintstagedrc.json
{
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"]
}
Критически важные правила ESLint для сохранения дисциплины:
@typescript-eslint/no-explicit-any: запрещает явное использованиеany.@typescript-eslint/ban-ts-comment: запрещает@ts-ignore, допуская@ts-expect-errorтолько с обязательным текстовым обоснованием.@typescript-eslint/no-floating-promises: защищает от пропущенныхawaitпри работе с асинхронными вызовами.
Метрики прогресса миграции
Для отслеживания динамики перехода в CI-пайплайн внедряют утилиту type-coverage. Она вычисляет процент выражений с известными (отличными от any) типами:
npx type-coverage --detail --strict --min 85
Флаг --min 85 остановит пайплайн, если процент покрытия типами опустится ниже 85%. По мере миграции эту планку постепенно поднимают до 95–98%.
Часто задаваемые вопросы (FAQ)
Можно ли включить strict: true только для новых файлов .ts, оставив старые нестрогими?
Компилятор применяет настройки tsconfig.json ко всему скоупу проекта. Однако разделить требования можно:
- Через Project References (несколько
tsconfig.jsonдля разных директорий). - Через конфигурацию
@typescript-eslint, применяя строгие правила линтинга только к новым папкам. - Через временные директивы
// @ts-expect-errorв легаси-модулях.
Почему @ts-expect-error надежнее @ts-ignore?
@ts-ignore подавляет ошибки бессрочно. Директива @ts-expect-error автоматически вызовет ошибку сборки, когда типизация строки станет корректной, сигнализируя разработчику о необходимости удалить устаревшую пометку.
Что делать, если у библиотеки нет пакета @types?
Создайте файл .d.ts (например, src/types/declarations.d.ts) с блоком declare module 'имя-библиотеки'. На старте достаточно описать только те методы и поля, которые используются в вашем приложении.
Зачем нужен noUncheckedIndexedAccess, если включен strict: true?
По умолчанию TypeScript предполагает, что доступ по индексу массива array[0] всегда возвращает T. На практике массив может быть пустым. Флаг noUncheckedIndexedAccess приводит тип к T | undefined, защищая от ошибок при чтении несуществующих элементов.
Нужно ли переписывать архитектуру модуля при смене расширения с .js на .ts?
Нет. Достаточно сменить расширение файла, прописать типы входных параметров функций и устранить ошибки компилятора. Глубокий рефакторинг внутренней логики безопаснее проводить отдельной итерацией, когда модуль уже находится под защитой типов.
Вывод
Миграция кодовой базы на TypeScript со стратегией strict: true — это планомерное управление техническим долгом, а не разовая кампания по массовому переименованию файлов.
Устойчивый результат опирается на три принципа:
- Изоляция проверок: быстрые сборщики собирают проект без задержек, а компилятор
tscпроверяет типы параллельно. - Порядок «снизу вверх»: утилиты и модели данных типизируются раньше, чем компоненты интерфейса и глобальное состояние.
- Контроль периметра: запрет неконтролируемого
any, отслеживаниеtype-coverageи автоматизация проверок в CI/CD не позволяют кодовой базе деградировать.
Такая стратегия исключает скрытые рантайм-баги, сохраняет стабильность релизного цикла и делает проект масштабируемым для всей команды.




.svg.webp)




