Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

Миграция с JavaScript на TypeScript: пошаговая стратегия внедрения `strict: true` без остановки разработки

Тимофей Ищенко

Коротко: Пошаговое руководство по миграции 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-файлов, не выполняя их полную валидацию при сборке.

Чтобы гарантировать корректность типов в проекте, проверку выносят в отдельный процесс:

  1. В локальном dev-окружении для Vite используют vite-plugin-checker, а для Webpack — fork-ts-checker-webpack-plugin.
  2. В 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-семейства.

Поочередное включение проверок

  1. noImplicitAny: true — первоочередной шаг. Устраняет слепые зоны, требуя явной типизации параметров функций и модульных экспортов.
  2. strictBindCallApply и noImplicitThis — отлавливают проблемы с передачей контекста исполнения функций.
  3. 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('Непредвиденный сбой');
  }
}
  1. strictNullChecks: true — наиболее масштабный этап, требующий явного сужения типов (type narrowing) и безопасной работы с опциональными цепочками (?.).
  2. 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 ко всему скоупу проекта. Однако разделить требования можно:

  1. Через Project References (несколько tsconfig.json для разных директорий).
  2. Через конфигурацию @typescript-eslint, применяя строгие правила линтинга только к новым папкам.
  3. Через временные директивы // @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 — это планомерное управление техническим долгом, а не разовая кампания по массовому переименованию файлов.

Устойчивый результат опирается на три принципа:

  1. Изоляция проверок: быстрые сборщики собирают проект без задержек, а компилятор tsc проверяет типы параллельно.
  2. Порядок «снизу вверх»: утилиты и модели данных типизируются раньше, чем компоненты интерфейса и глобальное состояние.
  3. Контроль периметра: запрет неконтролируемого any, отслеживание type-coverage и автоматизация проверок в CI/CD не позволяют кодовой базе деградировать.

Такая стратегия исключает скрытые рантайм-баги, сохраняет стабильность релизного цикла и делает проект масштабируемым для всей команды.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также