Многие команды сталкиваются с дилеммой: как интегрировать TypeScript в унаследованный JavaScript-проект, не останавливая разработку и не переписывая всё с нуля. Ошибочный подход «big bang» часто приводит к месяцам заморозки фич и болезненному рефакторингу. Рассмотрим инженерно оправданный метод миграции, совместимый с активной разработкой.
Зачем тратить ресурсы?
TypeScript – не просто аннотации типов. Это:
- Контракт для модулей: Четкие интерфейсы исключают классы ошибок вроде
undefined is not a function
на этапе компиляции. - Документация в коде: Типы заменяют 80% JSDoc и остаются актуальными.
- Покрытие легаси-кода: Позволяет обнаруживать скрытые баги при добавлении типов к старым модулям.
Пример:
// До
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// Риск: item без price, quantity или массив undefined
Типизированная версия сразу выявляет риски:
interface CartItem {
price: number;
quantity: number;
}
function calculateTotal(items: CartItem[]): number { ... }
Инкрементальная стратегия
Ключевой принцип: «Разрешить, затем усилить».
Шаг 1: Настройка среды
Установите зависимости:
npm install typescript @types/node --save-dev
Создайте tsconfig.json
с критически важными параметрами:
{
"compilerOptions": {
"allowJs": true, // Разрешить .js файлы
"checkJs": false, // Не проверять JS сразу
"outDir": "./dist",
"module": "ESNext",
"target": "ES2020",
"strict": false, // Пока без строгой проверки
"esModuleInterop": true
},
"include": ["src/**/*"]
}
Добавьте в package.json
:
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
}
Шаг 2: Захват файлов по принципу «островков»
Переименуйте файлы в .ts
по мере их доработки. Не делайте массового переименования! Начните с:
- Новых модулей
- Файлов с высокой цикломатической сложностью
- Утилит, используемых в 10+ местах
Пример преобразования:
// legacy-utils.js -> legacy-utils.ts
export function formatDate(date) { ... }
Становится:
export function formatDate(date: Date | string): string {
// ...
}
Шаг 3: Типизация через JSDoc (для временного решения)
Для файлов, оставшихся .js
, используйте JSDoc-аннотации. TypeScript их распознает:
/**
* @param {number[]} values
* @returns {number}
*/
function calculateAverage(values) { ... }
Добавьте проверку таких файлов через // @ts-check
в первой строке.
Шаг 4: Работа с внешними зависимостями
Проблема: Библиотека без типов в @types
. Решение – декларации .d.ts
:
// types/legacy-lib.d.ts
declare module "legacy-lib" {
export function riskyOperation(data: any): void;
}
Для модулей с неявным контрактом используйте утилиты:
// Упрощенная типизация для 300-строчного jQuery-плагина
type JQueryPlugin = (element: HTMLElement, options?: Record<string, unknown>) => void;
declare const initCustomCarousel: JQueryPlugin;
Шаг 5: Активация строгого режима
После покрытия 70+% кода типами включайте постепенно строгие проверки в tsconfig
:
{
"strict": true,
"strictNullChecks": true,
"noImplicitAny": false // Сначала отключено
}
Пошагово исправляйте ошибки, подключая правила:
npx tsc --strictNullChecks --noEmit src/modules/data-layer
Реальные сложности и решения
- Глобальные переменные:
declare global {
interface Window {
_env: { API_URL: string };
}
}
- Динамические ключи объектов:
type User = Record<string, unknown>; // Плохо
type User = {
id: number;
email: string;
[key: `custom_${string}`]: string; // Динамические кастомные поля
};
- Миграция тестов: Используйте
ts-node
с AVA/Jest:
// jest.config.js
module.exports = {
preset: "ts-jest",
testMatch: ["**/*.test.{js,ts}"] // Запускает и JS, и TS тесты
};
После миграции: что дальше?
- Включите
noImplicitAny
иstrictPropertyInitialization
. - Замените JSDoc аннотации на нативные типы.
- Внедрите утилиты типа вместо ручных проверок:
// Вместо: function getValue(): string | null
function getValue<T>(): T | null { ... }
Типизация – не религия, а инженерный инструмент. Грамотная миграция сокращает количество ошибок в production на 15-38% (исследования Microsoft). Начните с критических модулей, терпеливо добавляйте типы к легаси и помните: даже частичная типизация лучше, чем её отсутствие.
Итоговая рекомендация: Файл считается мигрированным, когда:
- Он в
.ts
- Проходит
strict:true
- Имеет ≥ 90% покрытие типов (отслеживается через
typescript-coverage-report
)
Инкрементальный подход позволяет получать пользу от TypeScript уже через неделю, а не через квартал.