← Назад в блог

Как перейти с JavaScript на TypeScript в существующем проекте без боли

Минимальный tsconfig, strict по шагам, миграция по файлам, типизация API и props. Частые ошибки с any и реальный пример рефактора JS → TS.

Как перейти с JavaScript на TypeScript в существующем проекте без боли

Требования

  • Существующий проект на JavaScript (Node или фронт)
  • Базовые знания JS (ES6+)

Как перейти с JavaScript на TypeScript в существующем проекте без боли

Миграция без «переписать всё разом»: минимальный tsconfig, включение strict по шагам, перенос по одному файлу, типизация API и props, типичные ловушки с any и один реальный пример рефактора модуля.


Минимальный tsconfig

В корне проекта создай tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "outDir": "dist",
    "rootDir": ".",
    "strict": false,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowJs": true,
    "checkJs": false,
    "noEmit": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Зачем так на старте миграции:

  • allowJs: true — в одном проекте могут быть и .js, и .ts; TypeScript проверяет оба.
  • checkJs: false — существующие .js пока не проверяем, только новые/переименованные .ts.
  • strict: false — включаем позже по одному флагу, иначе сразу получишь сотни ошибок.
  • noEmit: true — если сборку делает Vite/Webpack, компилятор только проверяет типы; при необходимости смени на "outDir" и убери noEmit.

Подставь под свою структуру: свой rootDir/include (например "src/**/*"), при сборке через bundler часто оставляют noEmit: true.


Strict-режим: включать по шагам

Вместо "strict": true сразу лучше включить по одному флагу и чинить по мере появления ошибок.

{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,
    "strictNullChecks": false,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false
  }
}

Порядок, который обычно не ломает всё разом:

  1. noImplicitAny: true — запрещает неявный any; заставляет явно типизировать или ставить unknown.
  2. Потом strictNullChecks: true — самый «шумный», включи когда основная часть кода уже на TS и null/undefined обработаны.
  3. Остальное (strictFunctionTypes, noUnusedLocals и т.д.) — по желанию.

В итоге к полному strict можно прийти за несколько итераций, не останавливая разработку.


Миграция по файлам

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

  1. Установка TypeScript (если ещё нет):
    npm i -D typescript
  2. Добавить tsconfig.json с allowJs: true, checkJs: false (как выше).
  3. Переименовать один выбранный файл: utils.jsutils.ts.
  4. Запустить проверку типов: npx tsc --noEmit (или через скрипт в package.json).
  5. Исправить ошибки в этом файле (типы, интерфейсы, приведение).
  6. Повторять для следующих файлов: чаще начинают с утилит и API, потом экраны и компоненты.

Пока в проекте есть .js, оставляй allowJs: true. Когда весь код станет .ts, можно включить checkJs: true для оставшихся .js (если они ещё есть) или удалить их.

Импорты из .ts в .js и наоборот работают без расширения: import { fn } from './utils' резолвится и для utils.js, и для utils.ts.


Типизация API

Ответы API типизируй интерфейсами или типами; так и автодополнение, и ошибки при изменении контракта будут в одном месте.

Пример типа ответа и функции запроса:

// types/api.ts
export interface User {
  id: number;
  email: string;
  name: string;
}

export interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}

export async function fetchUser(id: number): Promise<ApiResponse<User>> {
  const res = await fetch(`/api/users/${id}`);
  const json = (await res.json()) as ApiResponse<User>;
  if (!res.ok) {
    throw new Error(json.message ?? 'Request failed');
  }
  return json;
}

Использование:

const { data } = await fetchUser(1);
// data — User, есть data.id, data.email, data.name

Если бэкенд отдаёт «сырой» JSON и ты не доверяешь форме, можно описать только то, что реально используешь, или использовать unknown и сужать тип после проверки:

function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'email' in obj
  );
}
const raw = await res.json();
if (!isUser(raw)) throw new Error('Invalid user');
// raw — User

Типизация props (React)

Типизируй props интерфейом или типом; тогда и опечатки, и лишние/недостающие поля отловит компилятор.

До (JS):

export function Card({ title, count, onClick }) {
  return (
    <div onClick={onClick}>
      <h3>{title}</h3>
      <p>{count}</p>
    </div>
  );
}

После (TS):

interface CardProps {
  title: string;
  count: number;
  onClick?: () => void;
}

export function Card({ title, count, onClick }: CardProps) {
  return (
    <div onClick={onClick}>
      <h3>{title}</h3>
      <p>{count}</p>
    </div>
  );
}

Опциональные поля — через ?. Дети:

interface LayoutProps {
  children: React.ReactNode;
  className?: string;
}

export function Layout({ children, className }: LayoutProps) {
  return <div className={className}>{children}</div>;
}

Для событий используй типы из React: React.MouseEvent<HTMLButtonElement>, React.ChangeEvent<HTMLInputElement> и т.д., чтобы не терять типизацию при передаче в обработчики.


Частые ошибки с any

1. Неявный any в аргументах

// Плохо: a и b — неявный any
function add(a, b) {
  return a + b;
}

Включи noImplicitAny: true и явно укажи тип или используй unknown и сужение:

function add(a: number, b: number): number {
  return a + b;
}

2. any как «быстрое решение»

// Плохо: теряется вся польза типов
const data: any = await response.json();

Лучше описать контракт ответа или использовать unknown + type guard:

const raw: unknown = await response.json();
if (!isUser(raw)) throw new Error('Invalid');
const data: User = raw;

3. any в дженериках

// Плохо: массив теряет тип элемента
const items: any[] = [];

Укажи тип элемента:

const items: User[] = [];
// или
const items: Array<{ id: number; name: string }> = [];

4. any в типах событий и ref

// Плохо
const handleClick = (e: any) => { ... };
const ref = useRef<any>(null);

Используй типы из React/DOM:

const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { ... };
const ref = useRef<HTMLInputElement | null>(null);

Правило: использовать any только там, где тип реально неизвестен и нельзя описать интерфейсом; в остальных случаях — конкретный тип или unknown с проверкой.


Реальный пример рефактора: один модуль JS → TS

Был такой модуль (JS):

// api/users.js
export async function getUsers() {
  const res = await fetch('/api/users');
  const json = await res.json();
  return json;
}

export function formatUserName(user) {
  return `${user.firstName} ${user.lastName}`.trim();
}

Проблемы: непонятно, что возвращает getUsers, что за поля у user; при изменении API ошибки появятся только в рантайме.

После рефактора (TS):

// api/users.ts
export interface User {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
}

export async function getUsers(): Promise<User[]> {
  const res = await fetch('/api/users');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const json = (await res.json()) as User[];
  return json;
}

export function formatUserName(user: User): string {
  return `${user.firstName} ${user.lastName}`.trim();
}

Что сделано:

  • Описан контракт User — один раз, переиспользуется в типах ответа и аргументов.
  • getUsers() явно возвращает Promise<User[]>; вызовы получают автодополнение и проверку.
  • formatUserName(user: User) — нельзя передать объект без firstName/lastName; при переименовании полей на бэкенде компилятор укажет все места, где нужно поправить код.

Файл переименован в .ts, импорты в других файлах менять не нужно (путь без расширения). Ошибки типов исправляются в этом модуле и в местах вызова.


Краткий чеклист миграции

  • В корне есть tsconfig.json с allowJs: true, при необходимости noEmit: true.
  • Strict включается по шагам (сначала noImplicitAny, потом при желании strictNullChecks и др.).
  • Миграция по файлам: один файл → переименовать в .ts → поправить ошибки → следующий.
  • API: типы/интерфейсы для ответов, при необходимости type guards для unknown.
  • Компоненты: типизированы props (и при необходимости state/ref/события).
  • Вместо «затыкания» ошибок через any — явные типы или unknown с проверкой.

Так переход с JavaScript на TypeScript в существующем проекте остаётся управляемым и не требует единовременного переписывания всего кода.

0 просмотров

Комментарии

Загрузка комментариев...
Пока нет комментариев. Будьте первым!