📘 TypeScript

Working with files in TypeScript: a detailed guide

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты пишешь сочинение в школьной тетради. Ты открываешь тетрадь, читаешь, что написал вчера, дописываешь новую страницу, исправляешь ошибки, а потом закрываешь тетрадь и убираешь в рюкзак. Завтра ты снова её откроешь — и всё, что ты написал, останется на месте.

Теперь представь, что твоя программа — это такой же «писатель». Когда программа работает, она может что-то считать, вычислить, запомнить. Но как только она закрывается — вся память исчезает, как будто ты вырвал страницу из тетради и выбросил. Чтобы данные остались после завершения программы, их нужно сохранить в файл — как записать в тетрадь и убрать в портфель.

Работа с файлами — это умение программы:

  • Читать данные из файла (открыть тетрадь и прочитать)
  • Записывать данные в файл (написать новую страницу)
  • Дописывать данные в конец файла (добавить запись после уже написанного)
  • Удалять или переименовывать файлы (вырвать страницу или подписать тетрадь заново)
Важно: В браузере (JavaScript на веб-странице) файлы читать напрямую с компьютера пользователя нельзя — это нарушение безопасности. Поэтому мы будем говорить о Node.js — среде, которая позволяет TypeScript работать с файловой системой компьютера, как обычная программа на Python или C++.

Зачем это нужно в реальной жизни? Вот несколько примеров:

  • Игровой сервер сохраняет прогресс игроков в файл, чтобы после перезагрузки игроки не потеряли свои достижения
  • Программа для заметок читает и записывает текст в файлы на диске
  • Логи (журналы событий) записываются в файл, чтобы администратор мог посмотреть, что происходило с программой ночью
  • Конфигурационные файлы хранят настройки программы (например, какой цвет темы выбрал пользователь)

В этой статье мы сосредоточимся именно на чтении файлов — как открыть файл, прочитать его содержимое и правильно обработать полученные данные.

Теория: как это работает изнутри

Чтобы понять, как компьютер читает файл, нужно немного заглянуть «под капот» операционной системы.

Файл — это просто последовательность байтов

Любой файл на диске — это не «текст» и не «картинка». Это просто длинная цепочка нулей и единиц (битов), сгруппированных по 8 штук в байты. Когда мы говорим «прочитать текстовый файл», мы просим операционную систему: «Возьми эти байты и покажи мне их как буквы, используя кодировку UTF-8».

Потоки (Streams)

Представь, что файл — это длинная-длинная лента с напечатанными буквами. Чтение файла похоже на то, как ты протягиваешь эту ленту через читающее устройство. Ты можешь:

  • Протянуть всю ленту сразу и получить весь текст целиком
  • Протягивать по кусочку и читать частями (это называется «потоковое чтение»)

В Node.js есть два основных способа чтения:

  • Синхронный — программа говорит «читай файл» и стоит на месте, ждёт, пока файл полностью прочитается. Только потом идёт дальше.
  • Асинхронный — программа говорит «начни читать файл, а когда закончишь — скажи мне». Программа не ждёт, а занимается другими делами. Когда чтение завершится, вызывается специальная функция (колбэк) или срабатывает Promise.
Важно: В современных приложениях (особенно на серверах) почти всегда используют асинхронное чтение. Если сервер будет синхронно читать файл для одного пользователя, все остальные пользователи будут ждать своей очереди. Асинхронное чтение позволяет серверу обслуживать тысячи пользователей одновременно.

Кодировки

Когда мы читаем текстовый файл, нужно указать, в какой кодировке записаны буквы. Самая популярная сегодня — UTF-8. Если открыть файл в неправильной кодировке, вместо букв будут «кракозябры» (например, "Привет" вместо "Привет").

Путь к файлу

Чтобы прочитать файл, нужно знать, где он лежит. Путь бывает:

  • Абсолютный — полный адрес от корня диска: C:UsersИмяDocumentsfile.txt (Windows) или /home/user/file.txt (Linux/Mac)
  • Относительный — путь относительно текущей папки, где запущена программа: ./data/file.txt или просто file.txt

Синтаксис в TypeScript: все формы и варианты

В Node.js работа с файлами сосредоточена в модуле fs (file system — файловая система). Чтобы его использовать, нужно импортировать:

import * as fs from 'fs';  // Импортируем всё, что есть в модуле fs

Или, если нужны только методы для работы с промисами (современный стиль):

import * as fs from 'fs/promises';  // Только асинхронные методы, возвращающие Promise

Основные методы для чтения файлов

Метод Описание Возвращает Когда использовать
readFileSync(path, encoding?) Синхронное чтение всего файла string или Buffer В скриптах, где не важна производительность, или при загрузке конфигурации при старте
readFile(path, encoding?, callback) Асинхронное чтение с колбэком void (результат в колбэке) В старом коде (стиль callback)
readFile(path, encoding?) (из fs/promises) Асинхронное чтение с Promise Promise<string> или Promise<Buffer> Современный стандарт, используется с async/await
createReadStream(path, options?) Потоковое чтение (частями) ReadStream Для больших файлов (видео, логи, архивы)

Параметры

  • path (string | Buffer | URL) — путь к файлу
  • encoding (string) — кодировка: 'utf-8', 'ascii', 'base64' и другие. Если не указать, вернётся Buffer (сырые байты)
  • callback (function) — функция, которая вызывается после завершения чтения: (err: Error | null, data: string | Buffer) => void

Обработка ошибок

При работе с файлами всегда есть риск, что что-то пойдёт не так:

  • Файл не существует
  • Нет прав на чтение
  • Файл занят другой программой
  • Диск переполнен

Поэтому каждое чтение файла должно быть обёрнуто в try...catch (для синхронного и async/await) или обрабатывать ошибку в колбэке.

Примеры кода — от простого к сложному

Пример 1: Синхронное чтение текстового файла (самый простой)

// Импортируем модуль fs для работы с файлами
import * as fs from 'fs';

// Указываем путь к файлу (относительный — ищем в той же папке, где запущен скрипт)
const filePath = './hello.txt';

try {
    // Пытаемся прочитать файл синхронно
    // Первый аргумент — путь, второй — кодировка (чтобы получить строку, а не Buffer)
    const data: string = fs.readFileSync(filePath, 'utf-8');
    
    // Выводим содержимое файла в консоль
    console.log('Содержимое файла:');
    console.log(data);
} catch (error) {
    // Если что-то пошло не так — ловим ошибку и выводим понятное сообщение
    console.error('Ошибка при чтении файла:', (error as Error).message);
}

Объяснение: Этот код — самый простой способ прочитать файл. Программа останавливается на строке readFileSync и ждёт, пока файл прочитается. Если файла нет — программа не упадёт, а выведет ошибку в блоке catch.

Пример 2: Асинхронное чтение с колбэком (старый стиль)

import * as fs from 'fs';

const filePath = './data.txt';

// Асинхронное чтение: программа не ждёт, а вызывает функцию, когда файл прочитается
fs.readFile(filePath, 'utf-8', (error: Error | null, data: string) => {
    // Этот колбэк вызовется ПОСЛЕ того, как файл будет прочитан
    if (error) {
        // Если ошибка — выводим её и выходим из функции
        console.error('Не удалось прочитать файл:', error.message);
        return;
    }
    
    // Если ошибки нет — выводим содержимое
    console.log('Файл прочитан асинхронно:');
    console.log(data);
});

// Эта строка выполнится ДО того, как файл будет прочитан!
console.log('Программа продолжает работать, пока файл читается...');

Объяснение: Обрати внимание на порядок вывода. Сначала в консоли появится "Программа продолжает работать...", а потом — содержимое файла. Это и есть асинхронность: программа не блокируется.

Пример 3: Асинхронное чтение с Promise и async/await (современный стиль)

// Используем версию с промисами
import * as fs from 'fs/promises';

// Создаём асинхронную функцию, чтобы использовать await
async function readFileAsync(): Promise<void> {
    const filePath = './config.json';
    
    try {
        // await — говорим "подожди, пока Promise не выполнится"
        // Код выглядит как синхронный, но на самом деле не блокирует программу
        const data: string = await fs.readFile(filePath, 'utf-8');
        
        console.log('Конфигурация загружена:');
        console.log(data);
    } catch (error) {
        console.error('Ошибка загрузки конфигурации:', (error as Error).message);
    }
}

// Вызываем функцию
readFileAsync();

console.log('Программа запущена и ждёт загрузки конфигурации...');

Объяснение: Это самый удобный способ. Код выглядит линейно (как синхронный), но работает асинхронно. await приостанавливает выполнение только внутри функции readFileAsync, не блокируя всю программу.

Пример 4: Чтение JSON-файла и работа с данными

import * as fs from 'fs/promises';

// Определяем интерфейс для структуры данных, которые мы ожидаем в JSON
interface User {
    id: number;
    name: string;
    email: string;
    isActive: boolean;
}

async function loadUsers(): Promise<void> {
    const filePath = './users.json';
    
    try {
        // Читаем файл как строку
        const rawData: string = await fs.readFile(filePath, 'utf-8');
        
        // Преобразуем строку JSON в объект JavaScript
        // JSON.parse — встроенная функция, которая парсит JSON
        const users: User[] = JSON.parse(rawData) as User[];
        
        // Теперь мы можем работать с данными как с обычным массивом объектов
        console.log(`Загружено ${users.length} пользователей:`);
        
        // Фильтруем только активных пользователей
        const activeUsers = users.filter(user => user.isActive);
        console.log('Активные пользователи:', activeUsers.map(u => u.name).join(', '));
        
        // Находим пользователя по ID
        const user = users.find(u => u.id === 2);
        if (user) {
            console.log(`Пользователь с ID 2: ${user.name} (${user.email})`);
        }
    } catch (error) {
        if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
            console.error('Файл users.json не найден. Создайте его в папке проекта.');
        } else if (error instanceof SyntaxError) {
            console.error('Ошибка в структуре JSON-файла. Проверьте синтаксис.');
        } else {
            console.error('Неизвестная ошибка:', (error as Error).message);
        }
    }
}

loadUsers();

Объяснение: JSON — самый популярный формат для хранения структурированных данных. Мы читаем файл, парсим его в объект и работаем с ним как с обычным массивом. Обрати внимание на обработку разных типов ошибок: отдельно ловим отсутствие файла (код ENOENT) и ошибку синтаксиса JSON.

Пример 5: Чтение больших файлов через поток (Stream)

import * as fs from 'fs';
import * as readline from 'readline';

// Путь к большому файлу (например, лог-файл на 1 ГБ)
const filePath = './big_log.txt';

// Создаём поток для чтения файла
// Высокий уровень буфера (highWaterMark) — 1 МБ (по умолчанию 64 КБ)
const readStream = fs.createReadStream(filePath, {
    encoding: 'utf-8',
    highWaterMark: 1024 * 1024  // Читать по 1 МБ за раз
});

// Создаём интерфейс для построчного чтения
const rl = readline.createInterface({
    input: readStream,
    crlfDelay: Infinity  // Распознавать и Windows (\r\n), и Unix (\n) переносы строк
});

let lineCount = 0;
let errorCount = 0;

// Событие 'line' срабатывает для каждой строки файла
rl.on('line', (line: string) => {
    lineCount++;
    
    // Ищем строки, содержащие слово "ERROR"
    if (line.includes('ERROR')) {
        errorCount++;
        console.log(`Найдена ошибка в строке ${lineCount}: ${line.substring(0, 100)}...`);
    }
});

// Событие 'close' срабатывает, когда файл полностью прочитан
rl.on('close', () => {
    console.log(`\nЧтение завершено. Всего строк: ${lineCount}, ошибок: ${errorCount}`);
});

// Обработка ошибок потока
readStream.on('error', (error: Error) => {
    console.error('Ошибка при чтении потока:', error.message);
});

console.log('Начинаем потоковое чтение большого файла...');

Объяснение: Если читать файл размером 1 ГБ через readFile, программа попытается загрузить его в оперативную память целиком — это может привести к её исчерпанию. Потоковое чтение позволяет обрабатывать файл по частям. Здесь мы используем readline для построчного чтения — это удобно для логов, CSV и других текстовых форматов.

Пример 6: Чтение бинарного файла (изображение, архив)

import * as fs from 'fs/promises';
import * as path from 'path';

async function readBinaryFile(): Promise<void> {
    const filePath = './image.jpg';
    
    try {
        // Не указываем кодировку — получим Buffer (сырые байты)
        const buffer: Buffer = await fs.readFile(filePath);
        
        // Buffer — это специальный объект для работы с бинарными данными
        console.log('Информация о файле:');
        console.log(`Размер файла: ${buffer.length} байт`);
        console.log(`Размер в килобайтах: ${(buffer.length / 1024).toFixed(2)} КБ`);
        console.log(`Размер в мегабайтах: ${(buffer.length / (1024 * 1024)).toFixed(2)} МБ`);
        
        // Проверяем сигнатуру файла (первые байты)
        // JPEG начинается с байтов 0xFF 0xD8
        if (buffer[0] === 0xFF && buffer[1] === 0xD8) {
            console.log('Файл является JPEG-изображением');
        } else {
            console.log('Файл не является JPEG-изображением');
        }
        
        // Получаем расширение файла с помощью модуля path
        const ext = path.extname(filePath);
        console.log(`Расширение файла: ${ext}`);
        
    } catch (error) {
        console.error('Ошибка чтения бинарного файла:', (error as Error).message);
    }
}

readBinaryFile();

Объяснение: Не все файлы — текст. Изображения, видео, архивы — это бинарные данные. Мы читаем их как Buffer и можем анализировать отдельные байты. В примере мы проверяем «магические байты» (сигнатуру формата) в начале файла.

Пример 7: Чтение нескольких файлов параллельно

import * as fs from 'fs/promises';
import * as path from 'path';

// Функция для чтения одного файла с обработкой ошибок
async function readFileSafe(filePath: string): Promise<string | null> {
    try {
        const data = await fs.readFile(filePath, 'utf-8');
        return data;
    } catch (error) {
        console.error(`Не удалось прочитать ${filePath}:`, (error as Error).message);
        return null;  // Возвращаем null, если файл не прочитался
    }
}

async function readMultipleFiles(): Promise<void> {
    // Список файлов для чтения
    const files = [
        './file1.txt',
        './file2.txt',
        './file3.txt',
        './file4.txt'  // Этого файла может не существовать
    ];
    
    // Запускаем все чтения параллельно с помощью Promise.all
    // Promise.all принимает массив промисов и ждёт, пока ВСЕ они завершатся
    const results = await Promise.all(
        files.map(file => readFileSafe(file))
    );
    
    // results — массив, где каждый элемент — содержимое файла или null
    console.log('Результаты чтения:');
    
    results.forEach((content, index) => {
        const fileName = path.basename(files[index]);  // Получаем только имя файла
        if (content === null) {
            console.log(`${fileName}: [ОШИБКА ЧТЕНИЯ]`);
        } else {
            // Выводим первые 50 символов каждого файла
            const preview = content.substring(0, 50).replace(/
/g, ' ');
            console.log(`${fileName}: "${preview}..." (${content.length} символов)`);
        }
    });
    
    // Считаем общее количество символов во всех прочитанных файлах
    const totalChars = results.reduce((sum, content) => sum + (content?.length || 0), 0);
    console.log(`\nВсего прочитано символов: ${totalChars}`);
}

readMultipleFiles();

Объяснение: Когда нужно прочитать несколько файлов, не стоит читать их по очереди (это медленно). Мы запускаем все чтения одновременно с помощью Promise.all. Если один файл не читается — это не ломает всю операцию, мы просто получаем null для этого файла.

Пример 8: Чтение CSV-файла с парсингом

import * as fs from 'fs/promises';

interface Student {
    id: number;
    name: string;
    grade: number;
    subject: string;
}

async function parseCSV(): Promise<void> {
    const filePath = './students.csv';
    
    try {
        // Читаем весь CSV-файл
        const rawData = await fs.readFile(filePath, 'utf-8');
        
        // Разбиваем на строки (убираем пустые строки в конце)
        const lines = rawData.split('
').filter(line => line.trim() !== '');
        
        if (lines.length < 2) {
            console.log('Файл пуст или содержит только заголовок');
            return;
        }
        
        // Первая строка — заголовок (проверяем, что это действительно CSV)
        const header = lines[0].split(',');
        console.log('Заголовки столбцов:', header);
        
        // Остальные строки — данные
        const students: Student[] = [];
        
        for (let i = 1; i < lines.length; i++) {
            const values = lines[i].split(',');
            
            // Проверяем, что в строке достаточно значений
            if (values.length < 4) {
                console.warn(`Строка ${i + 1} пропущена: недостаточно данных`);
                continue;
            }
            
            // Создаём объект студента
            const student: Student = {
                id: parseInt(values[0].trim(), 10),
                name: values[1].trim(),
                grade: parseInt(values[2].trim(), 10),
                subject: values[3].trim()
            };
            
            // Проверяем, что ID и оценка — числа
            if (isNaN(student.id) || isNaN(student.grade)) {
                console.warn(`Строка ${i + 1} пропущена: некорректные числа`);
                continue;
            }
            
            students.push(student);
        }
        
        console.log(`\nЗагружено ${students.length} студентов:`);
        
        // Вычисляем среднюю оценку
        const averageGrade = students.reduce((sum, s) => sum + s.grade, 0) / students.length;
        console.log(`Средняя оценка: ${averageGrade.toFixed(2)}`);
        
        // Находим лучшего студента
        const bestStudent = students.reduce((best, current) => 
            current.grade > best.grade ? current : best
        );
        console.log(`Лучший студент: ${bestStudent.name} (${bestStudent.grade} баллов)`);
        
        // Группируем по предметам
        const subjects = [...new Set(students.map(s => s.subject))];
        console.log('Предметы:', subjects.join(', '));
        
    } catch (error) {
        console.error('Ошибка при парсинге CSV:', (error as Error).message);
    }
}

parseCSV();

Объяснение: CSV — популярный формат для табличных данных. Мы вручную парсим его: разбиваем на строки, каждую строку — на значения по запятой. Важно проверять данные: не все строки могут быть корректными. В реальных проектах для CSV лучше использовать библиотеки вроде csv-parse, но для понимания принципов полезно написать парсер самому.

Сравнение с Python: чем похоже и чем отличается

Характеристика TypeScript (Node.js) Python
Импорт модуля import * as fs from 'fs' import os или from pathlib import Path
Синхронное чтение fs.readFileSync(path, 'utf-8') with open(path, 'r') as f: data = f.read()
Асинхронное чтение await fs.readFile(path, 'utf-8') async with aiofiles.open(path, 'r') as f: data = await f.read()
Потоковое чтение fs.createReadStream() + readline for line in file: (итерация по строкам)
Работа с JSON JSON.parse(data) json.loads(data)
Обработка ошибок try...catch + проверка error.code try...except + проверка error.errno
Пути к файлам path.join(), path.resolve() os.path.join(), Path.resolve()
Бинарные данные Buffer bytes

Ключевые отличия:

  1. Асинхронность по умолчанию: В Node.js асинхронное чтение — это стандарт. В Python синхронное чтение — норма, а асинхронное требует дополнительной библиотеки (aiofiles).
  2. Работа с путями: В Python есть удобный модуль pathlib с объектно-ориентированным подходом. В TypeScript используется функциональный path.
  3. Контекстные менеджеры: В Python конструкция with open(...) as f: автоматически закрывает файл. В TypeScript нужно явно закрывать поток или полагаться на сборщик мусора.
  4. Типизация: TypeScript требует указывать типы (например, string, Buffer), что делает код более надёжным, но и более многословным.

Типичные ошибки и подводные камни

1. Забыли указать кодировку

// ОШИБКА: без кодировки вернётся Buffer, а не строка
const data = fs.readFileSync('./file.txt');
console.log(data);  // Выведет: <Buffer 48 65 6c 6c 6f>

// ПРАВИЛЬНО:
const data = fs.readFileSync('./file.txt', 'utf-8');
console.log(data);  // Выведет: Hello

2. Файл не существует (ошибка ENOENT)

Самая частая ошибка новичков. Если файла нет — программа упадёт с ошибкой ENOENT: no such file or directory. Всегда обрабатывайте эту ситуацию.

// ПЛОХОЙ КОД (упадёт, если файла нет):
const data = fs.readFileSync('./config.txt', 'utf-8');

// ХОРОШИЙ КОД:
try {
    const data = fs.readFileSync('./config.txt', 'utf-8');
} catch (error) {
    if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
        console.log('Файл конфигурации не найден. Использую настройки по умолчанию.');
    } else {
        throw error;  // Другие ошибки пробрасываем дальше
    }
}

3. Путаница с относительными путями

Относительный путь считается от текущей рабочей директории (где был запущен процесс), а не от папки, где лежит скрипт. Это частая причина ошибок при запуске из разных мест.

// Если запустить скрипт из папки проекта: node dist/app.js
// То './data.txt' будет искаться в папке проекта, а не в dist/

// Решение: использовать абсолютный путь через __dirname
import * as path from 'path';

// __dirname — путь к папке, где находится текущий скрипт
const filePath = path.join(__dirname, 'data.txt');
const data = fs.readFileSync(filePath, 'utf-8');

4. Утечка памяти при чтении больших файлов через readFile

readFile загружает весь файл в оперативную память. Для файлов размером 2+ ГБ это может вызвать ошибку JavaScript heap out of memory. Используйте потоки (createReadStream) для больших файлов.

5. Игнорирование ошибок в асинхронном коде

// ПЛОХОЙ КОД: ошибка будет "проглочена"