← Курс/Миграция JS-проекта на TypeScript#194 из 257+25 XP

Миграция JS-проекта на TypeScript

Стратегия постепенной миграции

Не нужно переписывать всё сразу. TypeScript поддерживает пошаговую миграцию: вы можете добавлять типы файл за файлом.

Шаг 1: allowJs + checkJs

Начните с минимальных изменений в tsconfig.json:

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "noEmit": true
  },
  "include": ["src/**/*"]
}
  • allowJs — TypeScript видит и обрабатывает .js файлы
  • checkJs: false — пока не проверяем JS файлы на ошибки
  • Шаг 2: Включить checkJs

    {
      "compilerOptions": {
        "allowJs": true,
        "checkJs": true,
        "strict": false,
        "noImplicitAny": false
      }
    }

    JSDoc-аннотации начинают работать:

    // @ts-check (или checkJs: true в tsconfig)
    
    /**
     * @param {string} name
     * @param {number} age
     * @returns {{ name: string, age: number }}
     */
    function createUser(name, age) {
      return { name, age }
    }

    Шаг 3: Переименование файлов

    Переименовывайте по одному: .js.ts. Начните с утилит (листья дерева зависимостей):

    utils/date.js      → utils/date.ts      (нет зависимостей)
    utils/format.js    → utils/format.ts    (нет зависимостей)
    services/api.js    → services/api.ts    (зависит от utils)
    components/App.js  → components/App.tsx (зависит от всего)

    Шаг 4: Исправление ошибок с any

    При переименовании в .ts TypeScript начнёт жаловаться. Используйте any как временное решение:

    // Временно допустимо — исправим позже
    function processData(data: any) {
      return data.map((item: any) => item.value)
    }

    Шаг 5: Строгий режим постепенно

    Включайте строгие флаги по одному:

    // Этап 1:
    { "noImplicitAny": true }
    
    // Этап 2:
    { "noImplicitAny": true, "strictNullChecks": true }
    
    // Этап 3:
    { "strict": true }

    jsconfig → tsconfig

    Если в проекте есть jsconfig.json — это почти готовый tsconfig.json:

    // jsconfig.json (было)
    {
      "compilerOptions": {
        "baseUrl": ".",
        "paths": { "@/*": ["src/*"] }
      }
    }
    
    // tsconfig.json (стало) — добавляем TypeScript-специфичные опции
    {
      "compilerOptions": {
        "baseUrl": ".",
        "paths": { "@/*": ["src/*"] },
        "allowJs": true,
        "target": "ES2020",
        "module": "ESNext",
        "strict": true
      }
    }

    Инструменты для автоматизации

    **ts-migrate** — автоматическая конвертация JS → TS от Airbnb:

    npx @ts-morph/ts-morph
    npx ts-migrate migrate src/

    Добавляет any там где нет типов — потом исправляете постепенно.

    Типичные проблемы при миграции

    // 1. Динамические ключи:
    const key = 'name'
    obj[key]  // ошибка — используй Record<string, unknown>
    
    // 2. Функции с неизвестным количеством аргументов:
    function fn(...args: any[]) { }  // временное решение
    
    // 3. Сторонние библиотеки без типов:
    // @ts-ignore или установить @types/library-name
    
    // 4. JSON import:
    import data from './data.json'
    // tsconfig: "resolveJsonModule": true

    Примеры

    Симуляция процесса миграции: анализ JS-кода, добавление JSDoc, отслеживание прогресса миграции по файлам

    // Симулируем процесс миграции JS проекта на TypeScript.
    // Анализируем "файлы", определяем порядок миграции, отслеживаем прогресс.
    
    // --- Структура проекта (симуляция) ---
    const projectFiles = [
      {
        path: 'src/utils/date.js',
        deps: [],
        hasJsDoc: true,
        complexity: 'low',
        linesOfCode: 45
      },
      {
        path: 'src/utils/format.js',
        deps: [],
        hasJsDoc: false,
        complexity: 'low',
        linesOfCode: 30
      },
      {
        path: 'src/utils/validation.js',
        deps: ['src/utils/format.js'],
        hasJsDoc: true,
        complexity: 'medium',
        linesOfCode: 80
      },
      {
        path: 'src/api/client.js',
        deps: ['src/utils/format.js'],
        hasJsDoc: false,
        complexity: 'medium',
        linesOfCode: 120
      },
      {
        path: 'src/services/UserService.js',
        deps: ['src/api/client.js', 'src/utils/validation.js'],
        hasJsDoc: true,
        complexity: 'high',
        linesOfCode: 200
      },
      {
        path: 'src/components/UserForm.jsx',
        deps: ['src/services/UserService.js', 'src/utils/validation.js'],
        hasJsDoc: false,
        complexity: 'high',
        linesOfCode: 350
      },
      {
        path: 'src/App.jsx',
        deps: ['src/components/UserForm.jsx', 'src/services/UserService.js'],
        hasJsDoc: false,
        complexity: 'high',
        linesOfCode: 180
      }
    ]
    
    // --- Алгоритм топологической сортировки (порядок миграции) ---
    function topologicalSort(files) {
      const sorted = []
      const visited = new Set()
      const fileMap = new Map(files.map(f => [f.path, f]))
    
      function visit(file) {
        if (visited.has(file.path)) return
        // Сначала обрабатываем зависимости
        file.deps.forEach(dep => {
          if (fileMap.has(dep)) visit(fileMap.get(dep))
        })
        visited.add(file.path)
        sorted.push(file)
      }
    
      files.forEach(f => visit(f))
      return sorted
    }
    
    // --- Оценка сложности миграции ---
    function estimateMigrationEffort(file) {
      const complexityScore = { low: 1, medium: 2, high: 3 }[file.complexity]
      const jsDocBonus = file.hasJsDoc ? -0.5 : 0  // JSDoc упрощает миграцию
      const locScore = file.linesOfCode / 100
    
      const score = complexityScore + jsDocBonus + locScore
      const hours = Math.round(score * 2)
    
      return {
        score: Math.round(score * 10) / 10,
        estimatedHours: hours,
        priority: score < 2 ? 'start here' : score < 4 ? 'medium' : 'last'
      }
    }
    
    // --- Симуляция поэтапной миграции ---
    class MigrationTracker {
      constructor(files) {
        this.files = files
        this.migrated = new Set()
        this.phase = 1
      }
    
      getStatus(filePath) {
        return this.migrated.has(filePath) ? '✓ migrated' : '○ pending'
      }
    
      canMigrate(file) {
        // Можно мигрировать если все зависимости уже мигрированы
        return file.deps.every(dep => this.migrated.has(dep))
      }
    
      migrateFile(filePath) {
        const file = this.files.find(f => f.path === filePath)
        if (!file) return false
    
        if (!this.canMigrate(file)) {
          console.log(`  ✗ Cannot migrate ${filePath}: dependencies not migrated`)
          return false
        }
    
        this.migrated.add(filePath)
        const newPath = filePath.replace(/\.jsx?$/, m => m === '.jsx' ? '.tsx' : '.ts')
        console.log(`  ✓ Migrated: ${filePath} -> ${newPath}`)
        return true
      }
    
      getProgress() {
        return {
          total: this.files.length,
          done: this.migrated.size,
          percent: Math.round((this.migrated.size / this.files.length) * 100)
        }
      }
    }
    
    // --- Демонстрация ---
    
    console.log('=== Топологический порядок миграции ===')
    const sorted = topologicalSort(projectFiles)
    sorted.forEach((file, i) => {
      const effort = estimateMigrationEffort(file)
      console.log(`${i + 1}. ${file.path}`)
      console.log(`   deps: ${file.deps.length}, effort: ${effort.estimatedHours}h, priority: ${effort.priority}`)
    })
    
    console.log('\n=== Оценка трудозатрат ===')
    const totalHours = sorted.reduce((sum, f) => sum + estimateMigrationEffort(f).estimatedHours, 0)
    console.log('Общая оценка:', totalHours, 'часов')
    
    console.log('\n=== Процесс миграции ===')
    const tracker = new MigrationTracker(sorted)
    
    // Попытка мигрировать в неправильном порядке:
    console.log('Попытка мигрировать App.jsx первым:')
    tracker.migrateFile('src/App.jsx')  // не получится
    
    // Правильный порядок (начинаем с листьев):
    console.log('\nПравильный порядок:')
    sorted.forEach(file => tracker.migrateFile(file.path))
    
    const progress = tracker.getProgress()
    console.log(`\nПрогресс: ${progress.done}/${progress.total} файлов (${progress.percent}%)`)