Как перейти с JavaScript на TypeScript в существующем проекте без боли
Минимальный tsconfig, strict по шагам, миграция по файлам, типизация API и props. Частые ошибки с any и реальный пример рефактора JS → TS.
Требования
- Существующий проект на 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
}
}
Порядок, который обычно не ломает всё разом:
- noImplicitAny: true — запрещает неявный
any; заставляет явно типизировать или ставитьunknown. - Потом strictNullChecks: true — самый «шумный», включи когда основная часть кода уже на TS и null/undefined обработаны.
- Остальное (strictFunctionTypes, noUnusedLocals и т.д.) — по желанию.
В итоге к полному strict можно прийти за несколько итераций, не останавливая разработку.
Миграция по файлам
Стратегия: не переименовывать всё в .ts сразу, а переводить по одному файлу и чинить ошибки по мере появления.
- Установка TypeScript (если ещё нет):
npm i -D typescript - Добавить
tsconfig.jsonсallowJs: true,checkJs: false(как выше). - Переименовать один выбранный файл:
utils.js→utils.ts. - Запустить проверку типов:
npx tsc --noEmit(или через скрипт вpackage.json). - Исправить ошибки в этом файле (типы, интерфейсы, приведение).
- Повторять для следующих файлов: чаще начинают с утилит и 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 в существующем проекте остаётся управляемым и не требует единовременного переписывания всего кода.



Комментарии