Почему работа с файлами в TypeScript — это не страшно, а удобно
Когда я только начинал писать на TypeScript, меня пугала мысль: «А как же файлы? В браузере-то всё просто, а тут — сервер, диски, пути...». Но, как оказалось, работа с файлами и файловой системой в TypeScript — это как собрать конструктор LEGO: есть чёткие детали (модули fs и path), есть инструкция (типизация), и результат получается предсказуемым. Никаких сюрпризов, только чистый код.
В этой статье мы разберёмся, как читать, писать, переименовывать и удалять файлы, используя современный TypeScript для Node.js. Вы узнаете, почему стоит использовать fs/promises вместо старого доброго fs, как не запутаться в путях и как красиво обрабатывать ошибки. Поехали!
Подготовка: что нужно знать перед стартом
Прежде чем нырять в код, убедитесь, что у вас установлен Node.js (версия 14+ — там уже есть нативные промисы). Также создайте простой проект:
mkdir ts-files-demo
cd ts-files-demo
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
В файле tsconfig.json обязательно включите "module": "NodeNext" или "ESNext" и "target": "ES2022". Теперь вы готовы к работе с файловой системой TypeScript.
Импорты, которые пригодятся всегда
Самый правильный способ импортировать модули для работы с файлами сегодня — это:
import { readFile, writeFile, unlink, mkdir, access } from 'fs/promises';
import { join, dirname, basename, extname } from 'path';
import { fileURLToPath } from 'url';
Обратите внимание: мы не используем fs с колбэками. Только fs/promises — это современный стандарт, который отлично сочетается с async/await и типизацией TypeScript.
Чтение файла: от простого к безопасному
Начнём с классики — прочитаем текстовый файл. Допустим, у нас есть data.txt с каким-то содержимым.
import { readFile } from 'fs/promises';
async function readTextFile(filePath: string): Promise<string> {
try {
const content = await readFile(filePath, 'utf-8');
return content;
} catch (error) {
console.error('Ошибка при чтении файла:', error);
throw error;
}
}
// Использование
const data = await readTextFile('./data.txt');
console.log(data);
Просто, правда? Но в реальных проектах мы часто работаем с JSON. Давайте сделаем универсальную функцию с дженериком:
import { readFile } from 'fs/promises';
async function readJsonFile<T>(filePath: string): Promise<T> {
const raw = await readFile(filePath, 'utf-8');
return JSON.parse(raw) as T;
}
// Пример: читаем конфиг
interface Config {
port: number;
dbUrl: string;
}
const config = await readJsonFile<Config>('./config.json');
console.log(config.port); // тип number — TypeScript сам подскажет
Это и есть работа с файлами TypeScript в её лучшем виде: типизация защищает вас от глупых ошибок, а код остаётся читаемым.
Запись в файл: создаём и обновляем данные
Теперь научимся сохранять информацию. Функция writeFile перезаписывает файл, если он существует, или создаёт новый.
import { writeFile, mkdir } from 'fs/promises';
import { dirname } from 'path';
async function saveToFile(filePath: string, data: string): Promise<void> {
// Сначала убедимся, что папка существует
const dir = dirname(filePath);
await mkdir(dir, { recursive: true });
// Теперь пишем
await writeFile(filePath, data, 'utf-8');
console.log(`Файл ${filePath} успешно сохранён`);
}
// Сохраняем объект как JSON
const user = { name: 'Анна', age: 28, role: 'admin' };
await saveToFile('./output/user.json', JSON.stringify(user, null, 2));
Обратите внимание на { recursive: true } в mkdir — это создаст все вложенные папки, если их нет. Без этой опции пришлось бы создавать каждую папку вручную. Работа с путями TypeScript становится намного проще, когда используешь path и fs/promises вместе.
Дописывание в конец файла
Иногда нужно не перезаписать файл, а добавить строку в конец. Для этого есть appendFile:
import { appendFile } from 'fs/promises';
async function logMessage(message: string): Promise<void> {
const timestamp = new Date().toISOString();
await appendFile('./logs/app.log', `[${timestamp}] ${message}\n`, 'utf-8');
}
await logMessage('Сервер запущен');
await logMessage('Пользователь залогинился');
Идеально для логов, трекинга событий или записи результатов в реальном времени.
Удаление и перемещение файлов
Рано или поздно вам понадобится что-то удалить. Используем unlink для файлов и rmdir (или rm с опцией recursive) для папок.
import { unlink, rm } from 'fs/promises';
// Удаление одного файла
async function deleteFile(filePath: string): Promise<void> {
try {
await unlink(filePath);
console.log(`Файл ${filePath} удалён`);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
console.warn('Файл не найден, пропускаем');
} else {
throw error;
}
}
}
// Удаление папки со всем содержимым
async function deleteFolder(folderPath: string): Promise<void> {
await rm(folderPath, { recursive: true, force: true });
console.log(`Папка ${folderPath} удалена`);
}
Кстати, обработка ошибок файлов TypeScript — это отдельное искусство. Обратите внимание на проверку кода ENOENT (Error NO ENTry — файл не существует). Всегда проверяйте такие коды, чтобы не падать с неожиданными исключениями.
Проверка существования файла и прав доступа
До того как читать или писать, хорошо бы проверить, существует ли файл. Раньше использовали fs.exists, но он deprecated. Теперь правильный способ — access:
import { access, constants } from 'fs/promises';
async function fileExists(filePath: string): Promise<boolean> {
try {
await access(filePath, constants.F_OK);
return true;
} catch {
return false;