Руководство по миграции на TypeScript
Перевод JavaScript-проекта на TypeScript облегчает сопровождение кода и делает работу в редакторе гораздо удобнее.
Это руководство шаг за шагом проведёт вас через миграцию.
Этап подготовки
Оцените свою кодовую базу. Прежде чем начинать миграцию:
- Определите размер и сложность кодовой базы
- Опишите процесс сборки и зависимости
- Поищите уже имеющиеся описания типов (файлы
.d.ts) - Найдите критически важные участки, которые требуют особого внимания
Настройте систему контроля версий. Убедитесь, что у вас чистый git-репозиторий (или его аналог). Создайте ветку для миграции и зафиксируйте текущее состояние, чтобы всегда можно было сравнить или откатиться:
git checkout -b typescript-migrationgit add .git commit -m "Pre-TypeScript migration state"
Настройка
Установите TypeScript как зависимость для разработки вместе с описаниями типов для Node.js: npm install --save-dev typescript @types/node
Создайте tsconfig.json. Начните с базовой конфигурации в корне проекта:
"compilerOptions": {"target": "ES2020","module": "commonjs","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"outDir": "./dist","rootDir": "./src"},"include": ["src/**/*"],"exclude": ["node_modules"]
Примечание: задайте в target самое старое окружение, которое вам нужно поддерживать.
Подходы к миграции
Есть два способа выполнить миграцию.
Постепенная миграция. Переводите по одному файлу за раз, пока остальной код остаётся на JavaScript. Включите "allowJs": true и "checkJs": true в compilerOptions, чтобы компилятор принимал и проверял файлы .js рядом с файлами .ts. Лучший вариант для больших кодовых баз, где важно минимально нарушать работу.
Миграция целиком. Переименуйте все файлы .js в .ts и исправьте ошибки, например с помощью find src -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \;. Лучший вариант для небольших и средних проектов, а также для новых проектов (с нуля).
Примечание: для больших проектов настоятельно рекомендуется постепенный подход — он меньше нарушает работу и делает объём задач посильным.
Пошаговая миграция
1. Начните с настройки. Создайте tsconfig.json с такими рекомендуемыми параметрами — они позволяют TypeScript проверять ваш JavaScript, пока ещё ничего не генерируя:
"compilerOptions": {"target": "ES2020","module": "commonjs","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"outDir": "./dist","rootDir": "./src","allowJs": true,"checkJs": true,"noEmit": true},"include": ["src/**/*"],"exclude": ["node_modules", "dist"]
2. Включите проверку типов для JavaScript. Добавьте // @ts-check в начало JavaScript-файла — и TypeScript начнёт его проверять (см. первый фрагмент кода). Отключить проверку одной строки можно с помощью // @ts-ignore.
3. Переименуйте файлы в .ts. Начните с некритичных файлов. Один файл переименовывается командой mv src/utils/helpers.js src/utils/helpers.ts, а целый каталог (осторожно) — командой find src/utils -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \;.
4. Добавьте аннотации типов. Постепенно задавайте типы параметров, возвращаемых значений и объектов (см. второй фрагмент кода).
5. Обновите скрипты сборки и тестирования. Добавьте компиляцию TypeScript в package.json: "scripts": { "build": "tsc", "dev": "tsc --watch", "test": "jest" }. Убедитесь, что ваша система тестирования тоже умеет запускать файлы TypeScript.
Проверка типов в JavaScript-файле
С // @ts-check и типом из JSDoc присваивание числа переменной name будет отмечено как ошибка.
// @ts-check
/** @type {string} */
const name = 'John';
// TypeScript will catch this error
name = 42; // Error: Type '42' is not assignable to type 'string'
Добавление аннотаций типов
Одна и та же функция add до и после указания типов, а также интерфейс, использованный как тип возвращаемого значения.
// Before
function add(a, b) {
return a + b;
}
// After
function add(a: number, b: number): number {
return a + b;
}
// With interface
interface User {
id: number;
name: string;
email?: string;
}
function getUser(id: number): User {
return { id, name: 'John Doe' };
}
Инструменты для миграции
Часть работы за вас могут сделать специальные инструменты:
- ts-migrate — инструмент автоматической миграции с JavaScript на TypeScript:
npx ts-migrate-full .(репозиторий на GitHub) - TypeStat — преобразует JavaScript в TypeScript с соблюдением типобезопасности:
npx typestat(репозиторий на GitHub) - пакеты @types — описания типов для ваших зависимостей, например
npm install --save-dev @types/react @types/node(искать их можно на TypeSearch)
Рекомендации по миграции на TypeScript
Начинайте с малого и двигайтесь итерациями
- Начните со вспомогательных функций и компонентов, не связанных с интерфейсом
- Переводите по одному файлу или модулю за раз
- Делайте коммит после каждого удачного шага
Используйте возможности TypeScript — пусть вывод типов работает за вас там, где это возможно, применяйте объединения типов для гибкости и защитники типов для проверок во время выполнения (см. фрагмент кода).
Работа со сторонними библиотеками
- Устанавливайте пакеты
@typesдля своих зависимостей - Пишите файлы объявлений для библиотек, у которых нет типов
- Используйте
declare moduleдля расширения глобальных типов
Полезные возможности TypeScript
Вывод типов, псевдоним объединения типов и функция-защитник типа — три возможности, которые небольшими усилиями делают перенесённый код безопаснее.
// Use type inference where possible
const name = 'John'; // TypeScript infers 'string'
const age = 30; // TypeScript infers 'number'
// Use union types for flexibility
type Status = 'active' | 'inactive' | 'pending';
// Use type guards for runtime checks
function isString(value: any): value is string {
return typeof value === 'string';
}
Типичные трудности и их решения
Динамические свойства
Проблема: в JavaScript обычные объекты часто используют как словари, добавляя свойства позже. TypeScript сообщает, что такого свойства не существует.
Решение: используйте индексную сигнатуру или утверждение типа.
Работа с контекстом this
Проблема: внутри обычной функции обратного вызова this теряется.
Решение: используйте стрелочную функцию или явно привяжите this.
Динамические свойства: проблема
Добавление name к пустому литералу объекта — это ошибка, потому что у его типа нет такого свойства.
// Before
const user = {};
user.name = 'John'; // Error: Property 'name' does not exist
Динамические свойства: решение
Добавить свойство позволяет либо интерфейс с индексной сигнатурой, либо утверждение типа.
// Option 1: Index signature
interface User {
[key: string]: any;
}
const user: User = {};
user.name = 'John'; // OK
// Option 2: Type assertion
const user = {} as { name: string };
user.name = 'John'; // OK
Контекст this: проблема
Внутри обычной функции function, переданной в setTimeout, this больше не является экземпляром Counter.
class Counter {
count = 0;
increment() {
setTimeout(function() {
this.count++; // Error: 'this' is not defined
}, 1000);
}
}
Контекст this: решение
Стрелочная функция сохраняет окружающий this; как вариант, можно указать тип this как Counter и вызвать .bind(this).
// Solution 1: Arrow function
setTimeout(() => {
this.count++; // 'this' is lexically scoped
}, 1000);
// Solution 2: Bind 'this'
setTimeout(function(this: Counter) {
this.count++;
}.bind(this), 1000);
Заключение
Переход с JavaScript на TypeScript — это крупное, но окупающееся вложение в вашу кодовую базу. С этим руководством вы сможете сделать переход плавно и постепенно.
Главное:
- Начните с продуманного
tsconfig.json - Используйте
allowJsиcheckJsдля постепенной миграции - Пусть система типов находит ошибки на раннем этапе
- Обновите процесс сборки и тестирования, чтобы он поддерживал TypeScript
- Решайте типичные трудности с помощью показанных выше приёмов
Миграция — это процесс, а не разовое событие. Смешанная кодовая база на время перехода — это нормально; важно ровное движение вперёд при сохранении качества кода.
Готовы начать? Настройте TypeScript в своём проекте и постепенно добавляйте аннотации типов — компилятор сам подскажет путь к более надёжному коду. Подробнее см. официальное руководство по миграции на TypeScript.