40. Миграция JS → TS
TypeScript: Миграция JS → TS
Заголовок раздела «TypeScript: Миграция JS → TS»Привет, коллеги-разработчики! Добро пожаловать в новый урок Yasha Learn Code. Сегодня мы отправимся в увлекательное путешествие — миграцию существующего JavaScript-проекта на TypeScript. Это как переезд из старой, но уютной квартиры, где все лежало “где-то там”, в новую, современную, где для каждой вещи есть свое место, и вы точно знаете, что и где найдете.
Зачем это нужно? TypeScript дает нам статическую типизацию, что означает:
- Безопасность: Ловите ошибки до запуска кода.
- Масштабируемость: Большие проекты легче поддерживать.
- Улучшенный DevEx: Автодополнение, рефакторинг, навигация в IDE.
- Ясность: Код становится документацией сам по себе.
Вы уже освоили дженерики, утилити-типы и интерфейсы, так что готовы к более серьезным вызовам. Поехали!
🏗️ Подготовка к Переезду: Настройка Проекта
Заголовок раздела «🏗️ Подготовка к Переезду: Настройка Проекта»Первым делом, нам нужно подготовить “фундамент” для нашего нового дома. Это означает установку TypeScript и настройку файла tsconfig.json.
-
Установка TypeScript:
Окно терминала npm install typescript --save-dev# илиyarn add typescript --dev -
Инициализация
tsconfig.json: Создайте файлtsconfig.jsonв корне проекта. Это сердце конфигурации TypeScript.tsconfig.json {"compilerOptions": {"target": "es2020", // Целевая версия ECMAScript для компиляции"module": "commonjs", // Система модулей (например, CommonJS для Node.js)"outDir": "./dist", // Каталог для скомпилированных JS-файлов"rootDir": "./src", // Корневой каталог для исходных TS-файлов"strict": true, // Включает все строгие проверки типов (наша цель!)"esModuleInterop": true, // Позволяет импортировать CommonJS модули как ES-модули"skipLibCheck": true, // Пропускает проверку деклараций библиотек"forceConsistentCasingInFileNames": true, // Требует согласованности регистра имен файлов"allowJs": true, // Разрешает включать JS-файлы в компиляцию (для миграции)"checkJs": true, // Включает проверку типов для JS-файлов (очень полезно!)"noEmit": true // Не генерирует JS-файлы, только проверяет типы},"include": [ // Какие файлы включать в компиляцию"src/**/*.ts","src/**/*.js"],"exclude": [ // Какие файлы исключать"node_modules","**/*.test.ts"]}Особое внимание уделите
allowJsиcheckJs— это ваши лучшие друзья на начальных этапах миграции, позволяющие TypeScript работать с существующим JS-кодом, постепенно проверяя его.noEmit: trueпозволит вам сначала просто проверять типы, не изменяя сборку.
Шаг за Шагом: Постепенная Миграция
Заголовок раздела «Шаг за Шагом: Постепенная Миграция»Самый эффективный подход к миграции — постепенный. Не пытайтесь переписать все сразу. Это как ремонт: лучше делать по комнате, а не сносить все стены одновременно.
Migration Strategy Flowchart
Заголовок раздела «Migration Strategy Flowchart»flowchart TD Setup[1. Setup tsconfig.json\nallowJs: true, checkJs: true] --> Dependencies[2. Install @types/...\nDefinitelyTyped] Dependencies --> Conversion[3. Rename .js to .ts/.tsx\nLeaf modules first] Conversion --> Typing[4. Add basic Types & Interfaces\nFix immediate errors] Typing --> Strict[5. Enable strict: true\nOne flag at a time] Strict --> Refactor[6. Refactor & Deep Typing\nRemove all 'any']
subgraph Iteration [Cycle for each file] Conversion Typing end
style Setup fill:#e1f5fe,stroke:#01579b style Strict fill:#fff4dd,stroke:#d4a017 style Refactor fill:#ccffcc,stroke:#333Пошаговый процесс миграции: от настройки окружения до полного покрытия типами.
-
Начните с “самой безопасной” комнаты: Обычно это утилитарные функции, небольшие, независимые модули без большого количества внешних зависимостей.
-
Переименуйте
.jsв.ts(или.tsxдля React-проектов): Это самый первый и простой шаг. TypeScript сразу начнет указывать на места, где ему не хватает информации.Предположим, у нас есть такой JS-файл:
src/utils.js /*** @param {number[]} numbers* @returns {number}*/function sumArray(numbers) {return numbers.reduce((acc, current) => acc + current, 0);}/*** @param {object} user* @param {string} user.firstName* @param {string} user.lastName* @returns {string}*/function formatUser(user) {return `${user.firstName} ${user.lastName}`;}module.exports = {sumArray,formatUser};Вы видите комментарии JSDoc, которые уже дают какую-то информацию о типах. TypeScript умеет их понимать!
Теперь переименуем
src/utils.jsвsrc/utils.ts. БлагодаряallowJs: trueиcheckJs: trueвtsconfig.json, TypeScript уже мог бы проверять этот файл. Но переименование в.tsдает полный контроль.src/utils.ts // TypeScript теперь полностью проверяет этот файлfunction sumArray(numbers: number[]): number {return numbers.reduce((acc, current) => acc + current, 0);}interface User {firstName: string;lastName: string;}function formatUser(user: User): string {return `${user.firstName} ${user.lastName}`;}export {sumArray,formatUser};Мы заменили JSDoc на нативные TypeScript-типы и интерфейсы. Это делает код чище и предоставляет полную мощь TS.
Работа с Зависимостями: D.TS Файлы
Заголовок раздела «Работа с Зависимостями: D.TS Файлы»Ваш JS-проект, скорее всего, использует кучу сторонних библиотек. Как TypeScript узнает об их типах? Здесь на помощь приходят файлы объявлений типов (.d.ts).
-
Библиотеки с встроенными типами: Многие современные библиотеки (например,
react,vue,lodash-es) уже поставляются с файлами.d.ts. Просто установите их, и TypeScript автоматически их подхватит. -
Библиотеки без встроенных типов: Для старых или менее популярных библиотек сообщество создало репозиторий DefinitelyTyped. Типы из него устанавливаются как
@types/название-библиотеки.Например, для
lodash:Окно терминала npm install lodash --savenpm install @types/lodash --save-dev -
Создание своих
.d.tsдля внутренних JS-модулей: Если у вас есть часть проекта, которая останется на JS, но вы хотите, чтобы TS-модули могли с ней корректно взаимодействовать, вы можете написать для нее декларационный файл.Допустим, у нас есть старый JS-файл
legacyAuth.js:src/legacyAuth.js function login(username, password) {// ... сложная логика входаreturn { token: 'abc', userId: 123 };}function logout(token) {console.log('User logged out with token:', token);}module.exports = { login, logout };Чтобы использовать его в TS, создадим
src/legacyAuth.d.ts:src/legacyAuth.d.ts declare module './legacyAuth' { // Объявляем модуль с конкретным путемinterface AuthResult {token: string;userId: number;}function login(username: string, password: string): AuthResult;function logout(token: string): void;// Если используется export default, то:// export default function login(username: string, password: string): AuthResult;// export function logout(token: string): void;}Теперь в любом TS-файле вы можете импортировать
loginиlogoutс полной типизацией:src/app.ts import { login, logout } from './legacyAuth'; // TS теперь знает типы из .d.tsconst authData = login('user', 'pass');console.log(authData.token); // Автодополнение и проверка типов работают!// logout(123); // Ошибка: Argument of type 'number' is not assignable to parameter of type 'string'.logout(authData.token);
Углубляемся: Строгие Проверки и Рефакторинг
Заголовок раздела «Углубляемся: Строгие Проверки и Рефакторинг»После того как вы переименовали большинство файлов и добавили базовые типы, приходит время для “генеральной уборки” – включения строгого режима ("strict": true в tsconfig.json). Это как установить систему безопасности, которая будет ловить даже мелкие нарушения.
strict: true эквивалентен включению всех этих опций:
noImplicitAny: Запрещает неявномуany. Это самый частый “затык” при миграции.strictNullChecks: Требует явной обработкиnullиundefined.strictFunctionTypes: Более строгая проверка совместимости функций.strictPropertyInitialization: Проверяет инициализацию свойств классов.noImplicitThis: Запрещает неявномуthis.alwaysStrict: Компилирует файлы в строгом режиме JavaScript.
Давайте рассмотрим пример noImplicitAny и strictNullChecks:
// src/dataProcessor.js (наш исходный JS)function processData(config) { const data = config.data; if (config.transform) { return config.transform(data); } return data;}
function getUserFullName(user) { return `${user.firstName || ''} ${user.lastName || ''}`;}
module.exports = { processData, getUserFullName };Мигрируем в src/dataProcessor.ts и включаем strict: true.
interface ProcessingConfig<T, R = T> { data: T; transform?: (input: T) => R; // transform может отсутствовать}
function processData<T, R>(config: ProcessingConfig<T, R>): T | R { const data = config.data; if (config.transform) { return config.transform(data); // TypeScript знает, что transform определен } return data;}
interface UserProfile { firstName: string; lastName: string; middleName?: string; // middleName теперь опционален email: string | null; // email может быть строкой или null}
function getUserFullName(user: UserProfile): string { // Благодаря strictNullChecks, user.email?.toLowerCase() безопасен // А вот user.email.toLowerCase() выдал бы ошибку без проверки или Optional Chaining const emailSuffix = user.email ? ` (${user.email.toLowerCase()})` : '';
// Optional chaining (?.) и Nullish coalescing (??) для безопасного доступа return `${user.firstName} ${user.middleName ?? ''} ${user.lastName}${emailSuffix}`;}
export { processData, getUserFullName };Здесь мы:
- Явно типизировали
configс помощью дженерик-интерфейсаProcessingConfig. - Использовали опциональные свойства (
?) дляtransformиmiddleName. - Применили union-тип (
string | null) дляemail. - Использовали операторы
?(optional chaining) и??(nullish coalescing) для безопасной работы с потенциально отсутствующими значениями, которые требуютstrictNullChecks: true.
Типичные “Затыки” и Как с Ними Бороться
Заголовок раздела «Типичные “Затыки” и Как с Ними Бороться»В процессе миграции вы неизбежно столкнетесь с некоторыми распространенными проблемами.
-
“Property ‘x’ does not exist on type ‘Y’.” Это означает, что вы пытаетесь получить доступ к свойству, которое TypeScript не видит в данном типе.
- Решение: Добавьте свойство в интерфейс/тип, используйте Type Guard (например,
if ('x' in obj)) или Type Assertion ((obj as MyType).x).
// Пример: свойство, которого нетinterface Dog {name: string;breed: string;}const myPet: Dog = { name: 'Buddy', breed: 'Golden Retriever' };// console.log(myPet.age); // Ошибка: Property 'age' does not exist on type 'Dog'.// Если вы уверены, что свойство есть (но TS не знает):const unknownData: any = { name: 'Whiskers', type: 'cat' };// console.log(unknownData.type.toUpperCase()); // Может быть ошибка в runtime, но TS не ругается, если unknownData: any// Правильное решение: Type Guardinterface Cat { name: string; type: 'cat'; }interface Fish { name: string; type: 'fish'; }type Pet = Dog | Cat | Fish;function getPetType(pet: Pet): string {if ('type' in pet) { // Type Guardreturn pet.type; // TS знает, что pet теперь имеет свойство 'type'}return pet.breed; // TS знает, что pet теперь Dog}console.log(getPetType({ name: 'Nemo', type: 'fish' })); // 'fish'console.log(getPetType(myPet)); // 'Golden Retriever' - Решение: Добавьте свойство в интерфейс/тип, используйте Type Guard (например,
-
“Object is possibly ‘null’ or ‘undefined’.” Это ошибка от
strictNullChecks, которая требует явной обработкиnullилиundefined.- Решение: Проверки (
if (value),value != null), опциональная цепочка (?.), оператор нулевого слияния (??), Non-null assertion operator (!, использовать с осторожностью!).
// Пример: Object is possibly 'null' or 'undefined'.function greetUser(user: { name: string; email?: string | null } | null): string {// return `Hello, ${user.name}!`; // Ошибка: Object is possibly 'null'.if (user === null) { // Явная проверка на nullreturn 'Hello, Guest!';}// Если email может быть undefined или null// const userEmail = user.email.toLowerCase(); // Ошибка: Object is possibly 'null' or 'undefined'.const userEmail = user.email?.toLowerCase() ?? 'no email provided'; // Безопасное использованиеreturn `Hello, ${user.name}! Your email: ${userEmail}`;}console.log(greetUser(null));console.log(greetUser({ name: 'Alice' }));console.log(greetUser({ name: 'Charlie', email: null }));// Non-null assertion operator (использовать очень осторожно, только если вы на 100% уверены!)const maybeString: string | undefined = "Hello";const definitelyString: string = maybeString!; // Говорим TS: "Я знаю, что это не undefined!" - Решение: Проверки (
-
Использование
anyкак временная мера: Иногда, особенно в начале миграции, вы можете столкнуться с очень сложными структурами данных или функциями, которые трудно сразу типизировать. В таких случаях, использованиеanyможет быть временным “костылем”.// src/complexLegacyComponent.js (много пропсов, которые трудно сразу типизировать)// ...// Временно в TSfunction renderComplexComponent(props: any) {// ... много логикиconsole.log(props.data.items[0].value); // TS не ругается, но и не проверяет// ...}💡 Важно:
anyдолжен быть временным решением, маячком для последующего рефакторинга. Его чрезмерное использование сводит на нет все преимущества TypeScript. Стремитесь минимизировать его и заменять на более точные типы (unknown, дженерики, конкретные интерфейсы) по мере продвижения.
💡 Совет
Заголовок раздела «💡 Совет»- Используйте Git: Миграция — это серия изменений. Коммитьте часто. Используйте ветки. Если что-то пошло не так, вы всегда сможете откатиться.
- Начните с листовых модулей: Сначала мигрируйте модули, которые не зависят ни от кого другого, или зависят только от типизированных библиотек. Затем переходите к модулям, которые зависят от уже мигрированных.
- Воспользуйтесь мощью вашей IDE: VS Code, WebStorm и другие IDE имеют потрясающую поддержку TypeScript. Используйте автодополнение, подсказки, автоматический рефакторинг.
- Используйте
"noEmit": trueна начальном этапе: Это позволит вам проверять типы, не заморачиваясь с изменением сборки проекта. Когда большая часть файлов будет типизирована, вы сможете настроить компиляцию вdist. - Не бойтесь
unknown: Если вы не уверены в типе, но не хотите терять безопасность, используйтеunknownвместоany.unknownзаставляет вас явно проверять тип перед использованием.
🎯 Практика
Заголовок раздела «🎯 Практика»Время применить полученные знания на практике!
Задание 1: Миграция Утилитарного Модуля
Заголовок раздела «Задание 1: Миграция Утилитарного Модуля»У вас есть JS-модуль для работы со строками. Мигрируйте его в TypeScript, добавив явные типы и интерфейсы.
/** * Преобразует строку в заголовочный регистр (каждое слово с большой буквы). * @param {string} str - Входная строка. * @returns {string} - Преобразованная строка. */function toTitleCase(str) { return str.replace(/\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); });}
/** * Генерирует уникальный ID. * @param {string} prefix - Префикс для ID (опционально). * @returns {string} - Уникальный ID. */function generateUniqueId(prefix) { return (prefix ? `${prefix}-` : '') + Math.random().toString(36).substr(2, 9);}
/** * Проверяет, является ли строка палиндромом. * @param {string} str - Входная строка. * @returns {boolean} */function isPalindrome(str) { const cleanedStr = str.toLowerCase().replace(/[^a-z0-9]/g, ''); return cleanedStr === cleanedStr.split('').reverse().join('');}
module.exports = { toTitleCase, generateUniqueId, isPalindrome };Ваша задача:
- Переименовать
stringUtils.jsвstringUtils.ts. - Добавить явные типы для параметров и возвращаемых значений функций.
- Создать интерфейс
StringUtilsдля модуля, если вы будете экспортировать его как объект.
Задание 2: Работа с Нетипизированным Внешним API
Заголовок раздела «Задание 2: Работа с Нетипизированным Внешним API»Представьте, что у вас есть JavaScript-функция, которая делает запрос к внешнему API. Этот API не предоставляет типов.
const fetch = require('node-fetch'); // Предположим, установлен node-fetch (npm install node-fetch)
async function fetchData(endpoint) { try { const response = await fetch(`https://api.example.com/${endpoint}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error('Failed to fetch data:', error.message); throw error; }}
// Пример использования (если бы это был JS)// fetchData('users/1').then(user => console.log(user.name));// fetchData('products/abc').then(product => console.log(product.price));
module.exports = { fetchData };Ваша задача:
- Мигрируйте
apiClient.jsвapiClient.ts. - Создайте
.d.tsфайл (например,apiClient.d.tsили прямо вapiClient.tsкакdeclare module) с объявлением типов для функцииfetchData. Используйте дженерики, чтобы функция могла возвращать разные типы данных в зависимости от эндпоинта. - Определите два интерфейса:
User(сid: number,name: string,email: string) иProduct(сid: string,name: string,price: number). - Создайте тестовый TS-файл (
main.ts), где вы вызоветеfetchDataдля полученияUserиProduct, демонстрируя, что типы теперь работают.
Задание 3: Очистка От any и null/undefined
Заголовок раздела «Задание 3: Очистка От any и null/undefined»В вашем проекте есть функция, которая была быстро написана и содержит много неявных any и потенциальных ошибок с null/undefined. Мигрируйте ее, сделав типобезопасной в строгом режиме.
function getDisplayName(userConfig) { if (!userConfig) { return 'Anonymous'; }
let name = userConfig.preferredName; if (!name && userConfig.details) { name = userConfig.details.firstName + ' ' + userConfig.details.lastName; }
if (name && name.length > 20) { return name.substring(0, 17) + '...'; } return name || 'Unknown User';}
module.exports = { getDisplayName };Ваша задача:
- Переименовать
profileManager.jsвprofileManager.ts. - Создать интерфейсы для
UserConfigиUserDetails, учитывая, что свойства могут быть опциональными илиnull/undefined. - Рефакторить функцию
getDisplayName, чтобы она полностью соответствовалаstrict: true(т.е. никаких ошибок “Object is possibly ‘null’ or ‘undefined’.” и “Implicit any”). Используйте опциональные цепочки, операторы нулевого слияния и другие Type Guard-ы по необходимости.