チュートリアル

TypeScript実践編:ファイル操作

TypeScriptNode.jsファイル操作実践
広告エリア

はじめに

TypeScript実践編では、型安全な実際の開発スキルを学びます。第1回はファイル操作です。

Node.jsのfsモジュールを型付きで使用します。

型定義のインストール

npm install --save-dev @types/node

基本的なファイル読み書き

ファイルを読む

import { readFile } from 'fs/promises';
import { createReadStream } from 'fs';
import * as readline from 'readline';

// ファイル全体を読み込む
async function readTextFile(filePath: string): Promise<string> {
  return await readFile(filePath, 'utf-8');
}

// 1行ずつ処理する
async function readLines(filePath: string): Promise<string[]> {
  const lines: string[] = [];
  const stream = createReadStream(filePath, 'utf-8');
  const rl = readline.createInterface({ input: stream });

  for await (const line of rl) {
    lines.push(line);
  }

  return lines;
}

ファイルに書き込む

import { writeFile, appendFile } from 'fs/promises';

// 新規作成・上書き
async function writeTextFile(filePath: string, content: string): Promise<void> {
  await writeFile(filePath, content, 'utf-8');
}

// 追記
async function appendToFile(filePath: string, content: string): Promise<void> {
  await appendFile(filePath, content, 'utf-8');
}

// 複数行を書き込み
async function writeLines(filePath: string, lines: string[]): Promise<void> {
  await writeFile(filePath, lines.join('\n') + '\n', 'utf-8');
}

型安全なJSON操作

TypeScriptの強みを活かした型安全なJSON操作です。

import { readFile, writeFile } from 'fs/promises';

// 型定義
interface User {
  name: string;
  age: number;
  skills: string[];
}

interface Config {
  version: string;
  debug: boolean;
  users: User[];
}

// 型安全なJSON読み込み
async function readJson<T>(filePath: string): Promise<T> {
  const content = await readFile(filePath, 'utf-8');
  return JSON.parse(content) as T;
}

// 型安全なJSON書き込み
async function writeJson<T>(filePath: string, data: T): Promise<void> {
  await writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
}

// 使用例
async function example() {
  // 型推論が効く
  const config = await readJson<Config>('config.json');
  console.log(config.version);  // 補完が効く

  const user: User = {
    name: '太郎',
    age: 25,
    skills: ['TypeScript', 'React']
  };
  await writeJson('user.json', user);
}

バリデーション付きJSON読み込み

実行時の型チェックにはzodなどを使用します。

import { readFile } from 'fs/promises';
import { z } from 'zod';

// スキーマ定義
const UserSchema = z.object({
  name: z.string(),
  age: z.number().min(0),
  skills: z.array(z.string())
});

type User = z.infer<typeof UserSchema>;

// バリデーション付き読み込み
async function readAndValidateJson<T>(
  filePath: string,
  schema: z.ZodType<T>
): Promise<T> {
  const content = await readFile(filePath, 'utf-8');
  const data = JSON.parse(content);
  return schema.parse(data);  // バリデーションエラーで例外
}

// 使用例
const user = await readAndValidateJson('user.json', UserSchema);

CSV操作

import { readFile, writeFile } from 'fs/promises';

interface CsvRow {
  [key: string]: string;
}

// CSVを読み込む
async function readCsv(filePath: string): Promise<CsvRow[]> {
  const content = await readFile(filePath, 'utf-8');
  const lines = content.trim().split('\n');
  const headers = lines[0].split(',');

  return lines.slice(1).map(line => {
    const values = line.split(',');
    return headers.reduce<CsvRow>((obj, header, i) => {
      obj[header] = values[i];
      return obj;
    }, {});
  });
}

// 型付きCSV読み込み
interface Employee {
  name: string;
  age: number;
  department: string;
}

async function readEmployeeCsv(filePath: string): Promise<Employee[]> {
  const rows = await readCsv(filePath);
  return rows.map(row => ({
    name: row.name,
    age: parseInt(row.age, 10),
    department: row.department
  }));
}

// CSVに書き込む
async function writeCsv<T extends Record<string, unknown>>(
  filePath: string,
  data: T[]
): Promise<void> {
  if (data.length === 0) return;

  const headers = Object.keys(data[0]);
  const lines = [
    headers.join(','),
    ...data.map(row => headers.map(h => String(row[h])).join(','))
  ];

  await writeFile(filePath, lines.join('\n'), 'utf-8');
}

パス操作

import * as path from 'path';

// パスの結合
const filePath: string = path.join('documents', 'report.txt');

// パス情報の取得
interface PathInfo {
  dir: string;
  name: string;
  ext: string;
  base: string;
}

function getPathInfo(filePath: string): PathInfo {
  const parsed = path.parse(filePath);
  return {
    dir: parsed.dir,
    name: parsed.name,
    ext: parsed.ext,
    base: parsed.base
  };
}

// 絶対パスに変換
const absolutePath: string = path.resolve('file.txt');

ディレクトリ操作

import {
  mkdir,
  readdir,
  stat,
  unlink,
  rm,
  copyFile,
  rename,
  access
} from 'fs/promises';
import { Dirent, Stats } from 'fs';

// 存在確認
async function exists(filePath: string): Promise<boolean> {
  try {
    await access(filePath);
    return true;
  } catch {
    return false;
  }
}

// ファイル情報の取得
async function getFileInfo(filePath: string): Promise<{
  isFile: boolean;
  isDirectory: boolean;
  size: number;
}> {
  const stats: Stats = await stat(filePath);
  return {
    isFile: stats.isFile(),
    isDirectory: stats.isDirectory(),
    size: stats.size
  };
}

// ディレクトリ作成
async function createDir(dirPath: string): Promise<void> {
  await mkdir(dirPath, { recursive: true });
}

// ディレクトリ内のファイル一覧
async function listFiles(dirPath: string): Promise<string[]> {
  return await readdir(dirPath);
}

// 詳細情報付き一覧
async function listFilesWithInfo(dirPath: string): Promise<{
  name: string;
  isDirectory: boolean;
}[]> {
  const entries: Dirent[] = await readdir(dirPath, { withFileTypes: true });
  return entries.map(entry => ({
    name: entry.name,
    isDirectory: entry.isDirectory()
  }));
}

// ファイル操作
async function fileOperations(): Promise<void> {
  await copyFile('src.txt', 'dst.txt');
  await rename('old.txt', 'new.txt');
  await unlink('file.txt');
  await rm('folder', { recursive: true });
}

ユーティリティ関数

汎用的なファイル操作ユーティリティです。

import { readFile, writeFile, mkdir } from 'fs/promises';
import * as path from 'path';

// ディレクトリを作成してからファイルを書き込む
async function writeFileWithDir(
  filePath: string,
  content: string
): Promise<void> {
  const dir = path.dirname(filePath);
  await mkdir(dir, { recursive: true });
  await writeFile(filePath, content, 'utf-8');
}

// ファイルを読み込み、なければデフォルト値を返す
async function readFileOrDefault(
  filePath: string,
  defaultValue: string
): Promise<string> {
  try {
    return await readFile(filePath, 'utf-8');
  } catch {
    return defaultValue;
  }
}

// JSONを読み込み、なければデフォルト値を返す
async function readJsonOrDefault<T>(
  filePath: string,
  defaultValue: T
): Promise<T> {
  try {
    const content = await readFile(filePath, 'utf-8');
    return JSON.parse(content) as T;
  } catch {
    return defaultValue;
  }
}

実践例:設定ファイル管理

import { readFile, writeFile } from 'fs/promises';

interface AppConfig {
  port: number;
  database: {
    host: string;
    port: number;
    name: string;
  };
  logging: {
    level: 'debug' | 'info' | 'warn' | 'error';
    file?: string;
  };
}

const DEFAULT_CONFIG: AppConfig = {
  port: 3000,
  database: {
    host: 'localhost',
    port: 5432,
    name: 'myapp'
  },
  logging: {
    level: 'info'
  }
};

class ConfigManager {
  private config: AppConfig;
  private configPath: string;

  constructor(configPath: string) {
    this.configPath = configPath;
    this.config = DEFAULT_CONFIG;
  }

  async load(): Promise<void> {
    try {
      const content = await readFile(this.configPath, 'utf-8');
      this.config = { ...DEFAULT_CONFIG, ...JSON.parse(content) };
    } catch {
      // ファイルがなければデフォルト設定を使用
      await this.save();
    }
  }

  async save(): Promise<void> {
    await writeFile(
      this.configPath,
      JSON.stringify(this.config, null, 2),
      'utf-8'
    );
  }

  get<K extends keyof AppConfig>(key: K): AppConfig[K] {
    return this.config[key];
  }

  set<K extends keyof AppConfig>(key: K, value: AppConfig[K]): void {
    this.config[key] = value;
  }
}

// 使用例
async function main() {
  const config = new ConfigManager('config.json');
  await config.load();

  console.log(config.get('port'));  // 3000
  config.set('port', 8080);
  await config.save();
}

まとめ

  • fs/promisesで型付き非同期ファイル操作
  • ジェネリクスで型安全なJSON読み書き
  • zodでランタイムバリデーション
  • pathモジュールでパス操作
  • ユーティリティ関数で再利用性を高める

次回は例外処理について学びます。

広告エリア