Хотите заказать веб-сайт? Связаться с нами

Миграция с JavaScript на TypeScript — первый шаг

В предыдущих уроках мы изучили все основные возможности TypeScript: аннотации типов, массивы и объекты, Union Types, Type Guards и Aliases. Теперь настало время применить эти знания на практике. Но что делать, если у вас уже есть существующий JavaScript-проект? Переписывать всё с нуля — слишком радикально и дорого.

TypeScript создан с учётом этого сценария. Он позволяет мигрировать постепенно, файл за файлом, без остановки разработки. Сегодня мы разберём, как сделать первый шаг в этом процессе.

Миграция с JavaScript на TypeScript — первый шаг

Стратегия миграции: постепенность — ключ к успеху

Миграция с JavaScript на TypeScript — это не спринт, а марафон. Вот основные принципы, которые я рекомендую:

  1. Начните с малого — переименуйте один файл с .js на .ts и посмотрите, что произойдёт.
  2. Используйте allowJs — разрешите TypeScript работать с вашими существующими .js файлами.
  3. Включайте проверки постепенно — начните с минимальных настроек и ужесточайте их по мере продвижения.
  4. Не переписывайте всё сразу — сосредоточьтесь на добавлении типов в критически важные части кода.

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

Самый простой способ начать миграцию — переименовать файл с расширением .js на .ts.

# Было
my-file.js

# Стало
my-file.ts

После этого TypeScript начнёт проверять этот файл. Если у вас есть зависимости от других .js файлов, они всё ещё могут работать, если вы включили опцию allowJs в конфигурации.

{
  "compilerOptions": {
    "allowJs": true,  // Разрешаем импорт из .js файлов
    "checkJs": false, // Пока не проверяем .js файлы
    "noEmit": true    // Не генерируем выходные файлы
  }
}

Шаг 2: TSX — инструмент для быстрой итерации

В процессе миграции вам может понадобиться быстро запускать TypeScript-файлы без полной компиляции. Для этого существует инструмент TSX (TypeScript Execute).

# Установка TSX
npm install -g tsx

# Запуск TypeScript файла
tsx my-file.ts

TSX работает как node, но с поддержкой TypeScript на лету. Он не выполняет строгую проверку типов, но позволяет быстро тестировать код во время миграции.

Разница между TSX и TSC:

  1. TSX — выполняет файлы напрямую, без проверки типов. Хорош для быстрой разработки.
  2. TSC — компилирует TypeScript в JavaScript и проверяет типы. Используется для продакшн-сборок.

Шаг 3: Первые ошибки — и как их исправлять

Когда вы переименуете файл в .ts, вы увидите ошибки, которые раньше были скрыты. Это нормально. TypeScript нашёл проблемы, которые существовали в вашем коде, но вы их не замечали.

Пример: Файл до миграции (user.js)

// user.js
function getUser(id) {
  return {
    id: id,
    name: "User " + id,
    email: null // Иногда здесь null
  };
}

function formatUser(user) {
  return user.name.toUpperCase() + " (" + user.email + ")";
}

const user = getUser(1);
console.log(formatUser(user));

После переименования в user.ts:

// user.ts
function getUser(id) {
  // ❌ Ошибка: Parameter 'id' implicitly has an 'any' type.
  return {
    id: id,
    name: "User " + id,
    email: null
  };
}

function formatUser(user) {
  // ❌ Ошибка: Parameter 'user' implicitly has an 'any' type.
  return user.name.toUpperCase() + " (" + user.email + ")";
  // ❌ Ошибка: 'user.email' может быть null
}

const user = getUser(1); // ❌ user имеет тип any
console.log(formatUser(user));
Из ноутбука появляются летающие светящиеся экраны

Оживи свой сайт. Освой JavaScript!

Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.

От теории — к реальным скриптам в твоём портфолио.

Подробнее о курсе

Шаг 4: Добавление аннотаций типов

Теперь мы начинаем добавлять типы, чтобы исправить ошибки. Начнём с самых очевидных мест:

// user.ts — первая итерация
function getUser(id: number): { id: number; name: string; email: string | null } {
  return {
    id: id,
    name: "User " + id,
    email: null
  };
}

function formatUser(user: { id: number; name: string; email: string | null }): string {
  // Но у нас всё ещё проблема с null
  return user.name.toUpperCase() + " (" + user.email + ")";
  // ❌ Ошибка: Object is possibly 'null'.
}

const user = getUser(1);
console.log(formatUser(user));

Шаг 5: Использование Type Guards и проверок

Чтобы исправить проблему с null, добавим проверку:

// user.ts — финальная версия
type User = {
  id: number;
  name: string;
  email: string | null;
};

function getUser(id: number): User {
  return {
    id: id,
    name: "User " + id,
    email: null
  };
}

function formatUser(user: User): string {
  // Используем Type Guard для проверки на null
  if (user.email === null) {
    return `${user.name.toUpperCase()} (email not provided)`;
  }
  return `${user.name.toUpperCase()} (${user.email})`;
}

const user: User = getUser(1);
console.log(formatUser(user));

Теперь код работает без ошибок и полностью типизирован!

Шаг 6: Type Aliases для переиспользования

Обратите внимание, что мы использовали псевдоним типа User. Это хорошая практика — выносить повторяющиеся структуры в отдельные типы.

// types.ts — файл для общих типов
export type User = {
  id: number;
  name: string;
  email: string | null;
};

export type Product = {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
};

// user.ts — импортируем типы
import { User } from "./types";

function getUser(id: number): User {
  // ...
}

Шаг 7: Использование JSDoc для постепенной миграции

Если вы не хотите полностью переходить на синтаксис TypeScript, вы можете использовать JSDoc-аннотации в .js файлах. TypeScript умеет читать их:

/**
 * @param {number} id
 * @returns {{ id: number; name: string; email: string | null }}
 */
function getUser(id) {
  return {
    id: id,
    name: "User " + id,
    email: null
  };
}

/**
 * @param {{ id: number; name: string; email: string | null }} user
 * @returns {string}
 */
function formatUser(user) {
  if (user.email === null) {
    return user.name.toUpperCase() + " (email not provided)";
  }
  return user.name.toUpperCase() + " (" + user.email + ")";
}

Если вы включите checkJs: true в tsconfig.json, TypeScript будет проверять JSDoc-аннотации в .js файлах, давая вам преимущества типизации без изменения синтаксиса.

Шаг 8: Постепенное ужесточение правил

По мере продвижения миграции вы можете ужесточать настройки TypeScript:

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,        // Теперь проверяем .js файлы
    "strict": true,         // Включаем все строгие проверки
    "noImplicitAny": true,  // Запрещаем неявный any
    "strictNullChecks": true, // Строгая проверка null/undefined
    "target": "ES2020",
    "module": "commonjs",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Практическое упражнение

Теперь ваша очередь попрактиковаться в миграции.

Задание: У вас есть JavaScript-файл shopping-cart.js. Ваша задача — мигрировать его на TypeScript, добавив все необходимые аннотации типов и исправив ошибки.

Исходный файл (shopping-cart.js):

// shopping-cart.js
function addToCart(cart, item) {
  const existingItem = cart.find(i => i.id === item.id);
  if (existingItem) {
    existingItem.quantity += 1;
  } else {
    cart.push({ ...item, quantity: 1 });
  }
  return cart;
}

function calculateTotal(cart) {
  let total = 0;
  for (const item of cart) {
    total += item.price * item.quantity;
  }
  return total;
}

function formatCart(cart) {
  if (cart.length === 0) {
    return "Cart is empty";
  }
  
  let result = "Shopping Cart:\n";
  for (const item of cart) {
    result += `- ${item.name}: $${item.price} x ${item.quantity} = $${item.price * item.quantity}\n`;
  }
  result += `Total: $${calculateTotal(cart)}`;
  return result;
}

// Использование
let cart = [];
cart = addToCart(cart, { id: 1, name: "Laptop", price: 999.99 });
cart = addToCart(cart, { id: 2, name: "Mouse", price: 49.99 });
cart = addToCart(cart, { id: 1, name: "Laptop", price: 999.99 });

console.log(formatCart(cart));

Решение (не подглядывайте сразу!):

// shopping-cart.ts
type CartItem = {
  id: number;
  name: string;
  price: number;
  quantity: number;
};

type Cart = CartItem[];

function addToCart(cart: Cart, item: Omit<CartItem, "quantity">): Cart {
  const existingItem = cart.find((i: CartItem) => i.id === item.id);
  if (existingItem) {
    existingItem.quantity += 1;
  } else {
    cart.push({ ...item, quantity: 1 });
  }
  return cart;
}

function calculateTotal(cart: Cart): number {
  let total = 0;
  for (const item of cart) {
    total += item.price * item.quantity;
  }
  return total;
}

function formatCart(cart: Cart): string {
  if (cart.length === 0) {
    return "Cart is empty";
  }
  
  let result = "Shopping Cart:\n";
  for (const item of cart) {
    const itemTotal = item.price * item.quantity;
    result += `- ${item.name}: $${item.price.toFixed(2)} x ${item.quantity} = $${itemTotal.toFixed(2)}\n`;
  }
  result += `Total: $${calculateTotal(cart).toFixed(2)}`;
  return result;
}

// Использование
let cart: Cart = [];
cart = addToCart(cart, { id: 1, name: "Laptop", price: 999.99 });
cart = addToCart(cart, { id: 2, name: "Mouse", price: 49.99 });
cart = addToCart(cart, { id: 1, name: "Laptop", price: 999.99 });

console.log(formatCart(cart));

/* Вывод:
Shopping Cart:
- Laptop: $999.99 x 2 = $1999.98
- Mouse: $49.99 x 1 = $49.99
Total: $2049.97
*/

Что мы сделали:

  1. Создали типы CartItem и Cart для описания структуры данных.
  2. Добавили аннотации для всех параметров и возвращаемых значений.
  3. Использовали Omit<CartItem, "quantity"> для параметра item, потому что при добавлении в корзину у товара ещё нет количества.
  4. Добавили форматирование чисел с toFixed(2) для корректного отображения цен.

Итог

Сегодня мы научились мигрировать JavaScript-код на TypeScript:

  1. Стратегия миграции — постепенность и работа с существующим кодом.
  2. Переименование файлов — первый шаг с .js на .ts.
  3. TSX — инструмент для быстрого запуска TypeScript-файлов без компиляции.
  4. Исправление ошибок — добавление аннотаций типов и Type Guards.
  5. JSDoc — альтернативный способ добавления типов в .js файлы.
  6. Постепенное ужесточение — настройка tsconfig.json по мере продвижения миграции.

Миграция — это процесс, который требует терпения. Не пытайтесь сделать всё сразу. Начните с одного файла, добавьте типы, убедитесь, что всё работает, и двигайтесь дальше. TypeScript создан для постепенной адаптации, и это одно из его главных преимуществ.

В следующем уроке мы закрепим полученные знания на практике — вас ждёт упражнение по добавлению аннотаций типов в реальный код.

Теги: