【TypeScript】Node.js + TypeScript 完全ガイド|環境構築・ts-node・tsx・@types/node・ESM設定まで徹底解説

【TypeScript】Node.js + TypeScript 完全ガイド|環境構築・ts-node・tsx・@types/node・ESM設定まで徹底解説 TypeScript

Node.js開発にTypeScriptを導入すると、関数の引数・戻り値・モジュールのインポートがすべて型チェックされ、大規模なサーバーサイドアプリケーションでも安全にリファクタリングできます。本記事では環境構築の3つの選択肢(tsc・ts-node・tsx)の比較から、Node.js向け tsconfig.json の最適設定・@types/node の使い方・CommonJS/ESMの切り替え・Express.jsとの統合・環境変数の型安全管理まで、実務で使えるNode.js + TypeScriptを完全解説します。

この記事でわかること

  • tsc・ts-node・tsx それぞれの特徴と使い分け
  • Node.js向け tsconfig.json の推奨設定
  • @types/node のインストールと fspathprocess の型定義
  • CommonJS(require)と ESM(import)の切り替え方法
  • Express.js + TypeScript の基本型定義パターン
  • 環境変数(process.env)を型安全に扱う方法
  • 実践例3本(CLIツール・HTTPサーバー・ファイル処理バッチ)
スポンサーリンク

1. Node.js + TypeScript のセットアップ方法を比較する

Node.jsでTypeScriptを実行する主な方法は3つあります。プロジェクトの規模・本番ビルドの必要性・実行速度によって使い分けます。

方法 概要 実行速度 本番利用 推奨用途
tsc + node TypeScriptをコンパイルしてからNodeで実行 遅い(コンパイル必要) ◎(標準的) 本番ビルドのある中〜大規模プロジェクト
ts-node TypeScriptをオンザフライで実行 中(初回は遅い) △(推奨しない) 開発・スクリプト・レガシープロジェクト
tsx esbuildベースの高速TS実行 速い △(本番はtscが推奨) 開発サーバー・スクリプト・新規プロジェクト

1-1. 方法①:tsc + node(本番標準)

TypeScriptコンパイラ(tsc)でJavaScriptにビルドし、node で実行する方法です。本番環境で最もシンプルかつ安全な構成です。

# 初期セットアップ
mkdir my-project && cd my-project
npm init -y
npm install -D typescript @types/node
npx tsc --init   # tsconfig.json を生成

# ビルドして実行
npx tsc          # src/*.ts → dist/*.js
node dist/index.js

# package.json scripts の例
# "build": "tsc",
# "start": "node dist/index.js",
# "dev":   "tsc --watch"

1-2. 方法②:ts-node(開発・スクリプト)

ts-node はTypeScriptファイルを直接実行できるツールです。コンパイルステップなしに node index.ts の感覚で動かせます。ただし実行のたびに型チェック+トランスパイルが走るため、起動が遅い場合があります。

# インストール
npm install -D ts-node typescript @types/node

# 直接実行
npx ts-node src/index.ts

# 型チェックをスキップして高速化(--transpile-only)
npx ts-node --transpile-only src/index.ts

# tsconfig.json の ts-node 設定
# {
#   "ts-node": {
#     "transpileOnly": true,   // 型チェックをスキップして高速化
#     "esm": true              // ESM を使う場合
#   }
# }

# package.json scripts の例
# "dev": "ts-node src/index.ts"
# "dev": "ts-node --transpile-only src/index.ts"

1-3. 方法③:tsx(高速・新規推奨)

tsx(TypeScript Execute)はesbuildをベースにした高速なTypeScript実行ツールです。ts-nodeより起動が速く、設定が少ない点が特長です。2024年以降の新規プロジェクトではts-nodeの代替として広く使われています。

# インストール
npm install -D tsx typescript @types/node

# 直接実行
npx tsx src/index.ts

# ウォッチモード(ファイル変更時に自動再実行)
npx tsx watch src/index.ts

# package.json scripts の例
# "dev":   "tsx watch src/index.ts",
# "start": "node dist/index.js",
# "build": "tsc"
推奨構成:開発は tsx、本番ビルドは tsc
新規プロジェクトでは devtsx watchbuildtsc を使う構成が現在のベストプラクティスです。tsxは型チェックをスキップしてトランスパイルのみ行うため、型エラーは別途 tsc --noEmitでチェックするか、CI/CDに組み込みましょう。

2. Node.js 向け tsconfig.json の推奨設定

Node.js用のtsconfig.jsonは、フロントエンド向けとは異なるオプションが必要です。targetmodulemoduleResolution の設定が特に重要です。

// tsconfig.json(Node.js向け推奨設定)
{
  "compilerOptions": {
    // ─── 出力設定 ───────────────────────────
    "target": "ES2022",          // Node.js 18+ は ES2022 が安全
    "module": "CommonJS",        // CommonJS の場合(require/module.exports)
    // "module": "NodeNext",     // ESM の場合(import/export)
    "moduleResolution": "node",  // CommonJS の場合
    // "moduleResolution": "NodeNext", // ESM の場合
    "outDir": "./dist",          // コンパイル先
    "rootDir": "./src",          // ソースルート

    // ─── 型チェック ─────────────────────────
    "strict": true,              // 厳格モード(推奨)
    "noUncheckedIndexedAccess": true, // arr[i] に undefined を含める
    "noImplicitReturns": true,   // 全パスで return を強制

    // ─── Node.js 固有 ────────────────────────
    "lib": ["ES2022"],           // DOM は不要
    "types": ["node"],           // @types/node を明示的に使用

    // ─── その他 ──────────────────────────────
    "esModuleInterop": true,     // import x from "x" 形式を使えるようにする
    "resolveJsonModule": true,   // import data from "./data.json" を有効化
    "skipLibCheck": true,        // 型定義ファイルのチェックをスキップ(ビルド高速化)
    "declaration": true,         // .d.ts を生成(ライブラリとして公開する場合)
    "sourceMap": true            // デバッグ用ソースマップを生成
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
オプション 説明 CommonJS ESM
target コンパイル後のJS版 ES2022 ES2022
module モジュール形式 CommonJS NodeNext
moduleResolution モジュール解決方法 node NodeNext
esModuleInterop default importの互換 true 推奨 true 推奨

2-1. target はNode.jsバージョンに合わせる

Node.jsバージョン 推奨 target 備考
Node.js 18.x (LTS) ES2022 ほぼすべてのES2022機能をサポート
Node.js 20.x (LTS) ES2022 または ES2023 Array.prototype.toSorted() 等が使用可能
Node.js 22.x ES2024 最新機能が利用可能
Node.js 14〜16 ES2019ES2021 古いLTSはES2022の一部機能が未サポート

3. @types/node の使い方

@types/node はNode.jsの組み込みモジュール(fspathhttpprocess 等)の型定義パッケージです。インストールするとNode.jsのAPIが型安全に使えるようになります。

npm install -D @types/node

3-1. fs モジュールの型安全な使い方

import fs from "fs";
import { promises as fsPromises } from "fs";
import path from "path";

// 同期: fs.readFileSync の戻り値は Buffer | string
const raw: Buffer = fs.readFileSync("./data.json");
const text: string = fs.readFileSync("./data.json", "utf-8");

// 非同期 (Promise): fsPromises
async function readJson(filePath: string): Promise<unknown> {
  const content = await fsPromises.readFile(filePath, "utf-8");
  return JSON.parse(content);
}

// ファイル存在確認
async function fileExists(filePath: string): Promise<boolean> {
  try {
    await fsPromises.access(filePath, fs.constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

// パス操作
const dir  = path.dirname("/home/user/file.txt");   // "/home/user"
const base = path.basename("/home/user/file.txt");  // "file.txt"
const ext  = path.extname("/home/user/file.txt");   // ".txt"
const abs  = path.resolve("./src", "index.ts");     // 絶対パスに変換
const join = path.join("a", "b", "c.txt");          // "a/b/c.txt"

3-2. process の型定義

// process.argv: string[]
const args: string[] = process.argv.slice(2); // 先頭2つ(node・スクリプトパス)を除く

// process.env: NodeJS.ProcessEnv(= Record<string, string | undefined>)
const port: string | undefined = process.env.PORT;
const portNum: number = port ? parseInt(port, 10) : 3000;

// process.cwd(): string(カレントディレクトリ)
const cwd: string = process.cwd();

// process.exit(): never(プロセス終了)
function exitWithError(msg: string): never {
  console.error(msg);
  process.exit(1);
}

// process.on(): イベントリスナー
process.on("SIGINT", () => {
  console.log("Shutting down...");
  process.exit(0);
});

process.on("uncaughtException", (err: Error) => {
  console.error("Uncaught exception:", err.message);
  process.exit(1);
});

3-3. http モジュールとカスタム型

import http from "http";

// IncomingMessage・ServerResponse の型が利用可能
const server = http.createServer(
  (req: http.IncomingMessage, res: http.ServerResponse) => {
    const url  = req.url ?? "/";
    const method = req.method ?? "GET";

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ url, method }));
  }
);

server.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

4. CommonJS と ESM の設定

Node.jsにはモジュール形式が2つあります。CommonJSrequire/module.exports)は従来形式で、ESMimport/export)はECMAScript標準形式です。TypeScriptで書く場合は常に import/export 構文を使えますが、出力形式は tsconfig.jsonmodule オプションで決まります。

比較項目 CommonJS ESM
tsconfig module CommonJS NodeNext または ES2022
package.json type 不要(または "type": "commonjs" "type": "module" が必要
ファイル拡張子 .js / .ts .mjs / .mts(または package.json type=module)
動的import require()(同期) import()(非同期)
__dirname / __filename 使用可能 使用不可(別途代替が必要)
npm パッケージ互換性 ほぼ全パッケージに対応 ESM専用パッケージのみ対応
推奨場面 既存プロジェクト・大半のnpmパッケージ 新規・モダンプロジェクト

4-1. CommonJS 設定(推奨・安定)

// tsconfig.json (CommonJS)
{
  "compilerOptions": {
    "module": "CommonJS",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "target": "ES2022"
  }
}
// CommonJS での import 記述(TypeScriptでは import 構文を使う)
import fs from "fs";           // esModuleInterop: true が必要
import path from "path";
import express from "express"; // OK

// __dirname と __filename は CommonJS では使用可能
console.log(__dirname);   // /home/user/project/src
console.log(__filename);  // /home/user/project/src/index.ts

4-2. ESM 設定

// package.json(ESM 有効化)
{
  "name": "my-project",
  "type": "module",
  "scripts": {
    "dev":   "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
// tsconfig.json (ESM)
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022"
  }
}
// ESM での注意: __dirname が使えない → import.meta.url で代替
import { fileURLToPath } from "url";
import path from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname  = path.dirname(__filename);

// ESM では拡張子を明示(NodeNext の場合)
import { myFunc } from "./utils.js";  // .ts ではなく .js を書く
// TypeScript が .ts → .js に変換するため、import では .js を指定
ESM + NodeNext で import "./utils" がエラーになる
"moduleResolution": "NodeNext" の場合、import パスには必ず拡張子を付ける必要があります。import "./utils" は NG で、import "./utils.js" と書きます。(TypeScriptは .ts ファイルを探しますが、import文には .js を書く)これは最初に戸惑いやすい落とし穴です。

TypeScriptのモジュールシステム全般(import type・パスエイリアス・動的import)については モジュールとimport/export完全ガイド も参照してください。

5. 環境変数(process.env)の型安全な管理

process.env の型は NodeJS.ProcessEnvRecord<string, string | undefined>)で、すべての値が string | undefined です。型安全に扱うにはバリデーション関数か、dotenv + 型定義ファイルを使います。

5-1. バリデーション関数パターン

// 環境変数を型安全に取得するユーティリティ
function getEnv(key: string): string {
  const value = process.env[key];
  if (value === undefined) {
    throw new Error(`環境変数 ${key} が設定されていません`);
  }
  return value;
}

function getEnvInt(key: string, defaultValue?: number): number {
  const value = process.env[key];
  if (value === undefined) {
    if (defaultValue !== undefined) return defaultValue;
    throw new Error(`環境変数 ${key} が設定されていません`);
  }
  const num = parseInt(value, 10);
  if (isNaN(num)) throw new Error(`環境変数 ${key} は整数でなければなりません`);
  return num;
}

// 起動時に一括チェックするパターン
interface AppConfig {
  port:       number;
  dbUrl:      string;
  jwtSecret:  string;
  nodeEnv:    "development" | "production" | "test";
}

function loadConfig(): AppConfig {
  const nodeEnv = process.env.NODE_ENV ?? "development";
  if (!["development", "production", "test"].includes(nodeEnv)) {
    throw new Error(`NODE_ENV の値が不正です: ${nodeEnv}`);
  }
  return {
    port:      getEnvInt("PORT", 3000),
    dbUrl:     getEnv("DATABASE_URL"),
    jwtSecret: getEnv("JWT_SECRET"),
    nodeEnv:   nodeEnv as "development" | "production" | "test",
  };
}

// アプリ起動時に一度だけ実行してキャッシュ
export const config = loadConfig();
// 以降は config.port, config.dbUrl と型安全にアクセスできる

5-2. dotenv と型定義ファイルの組み合わせ

.d.ts ファイルを使って NodeJS.ProcessEnv インターフェースを拡張することで、process.env の各変数に型を付けられます。型定義ファイルの詳細は 型定義ファイル(.d.ts)完全ガイド を参照してください。

// src/env.d.ts: process.env の型を拡張
declare namespace NodeJS {
  interface ProcessEnv {
    readonly NODE_ENV:      "development" | "production" | "test";
    readonly PORT?:         string;
    readonly DATABASE_URL:  string;
    readonly JWT_SECRET:    string;
    readonly REDIS_URL?:    string;
  }
}
// dotenv の使い方
// npm install dotenv
// npm install -D @types/dotenv (@types/node に含まれるため通常不要)

import "dotenv/config";  // .env ファイルを process.env に読み込む

// または
import dotenv from "dotenv";
dotenv.config();

// env.d.ts で型を定義している場合、process.env にアクセスすると型補完が効く
const port = process.env.PORT ?? "3000";  // string | "3000"
const db   = process.env.DATABASE_URL;    // string(undefinedなし)

6. Express.js + TypeScript の型定義

Node.jsの最もポピュラーなWebフレームワーク、Express.jsをTypeScriptで使う方法を解説します。@types/express をインストールすると、RequestResponseNextFunction の型が利用できます。

npm install express
npm install -D @types/express

6-1. 基本的なルーティングと型定義

import express, { Request, Response, NextFunction } from "express";

const app = express();
app.use(express.json());

// ─── ジェネリクスでRequest/Responseを型付け ───────
// Request<Params, ResBody, ReqBody, Query>
// Response<ResBody>

interface User {
  id: number;
  name: string;
  email: string;
}

// GET /users/:id
app.get<{ id: string }, User | { error: string }>(
  "/users/:id",
  async (req, res) => {
    const id = parseInt(req.params.id, 10);
    if (isNaN(id)) {
      res.status(400).json({ error: "IDは整数で指定してください" });
      return;
    }
    const user = await findUser(id);
    if (!user) {
      res.status(404).json({ error: "ユーザーが見つかりません" });
      return;
    }
    res.json(user);
  }
);

// POST /users
interface CreateUserBody {
  name: string;
  email: string;
}

app.post<{}, User, CreateUserBody>(
  "/users",
  async (req, res) => {
    const { name, email } = req.body;
    // req.body は CreateUserBody 型として扱われる
    const user = await createUser({ name, email });
    res.status(201).json(user);
  }
);

app.listen(3000, () => console.log("Server running on port 3000"));

6-2. ミドルウェアとリクエスト拡張

// Request に独自プロパティを追加する型拡張
// src/types/express/index.d.ts
declare global {
  namespace Express {
    interface Request {
      user?: {
        id:    number;
        email: string;
        role:  "admin" | "user";
      };
      requestId?: string;
    }
  }
}

// 認証ミドルウェア
function authMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (!token) {
    res.status(401).json({ error: "認証が必要です" });
    return;
  }
  // トークン検証後にユーザー情報を req.user に付与
  // (実装例: jwt.verify(token, secret) as { id, email, role } など)
  req.user = verifyToken(token); // 戻り値に { id, email, role } の型が付いている
  next();
}

// 使用: req.user は認証ミドルウェアを通過後に利用可能
app.get("/profile", authMiddleware, (req, res) => {
  // req.user は { id, email, role } | undefined
  if (!req.user) { res.status(401).json({ error: "Unauthorized" }); return; }
  res.json({ id: req.user.id, email: req.user.email });
});

エラーハンドリングミドルウェアの型定義については TypeScriptエラーハンドリング完全ガイド も参照してください。

7. よくあるエラーと解決方法

エラー 原因 解決策
Cannot find module 'fs' @types/node が未インストール、または tsconfig.jsontypes"node" がない npm install -D @types/node を実行し、tsconfig.json"types": ["node"] を確認
__dirname is not defined ESM("type": "module")環境で CommonJS 専用の __dirname を使っている import.meta.url + fileURLToPath で代替(セクション4-2参照)
Cannot use import statement in a module tsconfig.jsonmoduleCommonJS なのに import 構文の出力が require にならない、または逆 modulepackage.jsontype フィールドを合わせる
Type 'string | undefined' is not assignable to type 'string' process.env.FOOstring | undefined なのに string として使っている process.env.FOO ?? "default" でデフォルト値を設定するか、getEnv("FOO") 関数を使う
Relative import paths need explicit file extensions moduleResolution: NodeNext で拡張子なしの import を書いている import "./utils.js" のように .js 拡張子を明示する
ts-node: Cannot find module パスエイリアス(@/ など)を設定しているが tsconfig-paths を使っていない npm install -D tsconfig-paths して ts-node -r tsconfig-paths/register で実行

8. 実践例3本

実践例1:型安全なCLIツール

process.argv を型安全にパースし、サブコマンドとオプションを処理するCLIツールの実装例です。

#!/usr/bin/env node
import path from "path";
import { promises as fs } from "fs";

type Command = "create" | "delete" | "list";

interface CliOptions {
  command: Command;
  name?:   string;
  force:   boolean;
  verbose: boolean;
}

function parseArgs(args: string[]): CliOptions {
  const [command, ...rest] = args;
  if (!["create", "delete", "list"].includes(command)) {
    console.error(`不正なコマンド: ${command}`);
    console.error("使用方法: cli <create|delete|list> [name] [--force] [--verbose]");
    process.exit(1);
  }
  return {
    command: command as Command,
    name:    rest.find(a => !a.startsWith("--")),
    force:   rest.includes("--force"),
    verbose: rest.includes("--verbose"),
  };
}

async function run(opts: CliOptions): Promise<void> {
  const baseDir = path.resolve(process.cwd(), "output");

  switch (opts.command) {
    case "create": {
      if (!opts.name) { console.error("name が必要です"); process.exit(1); }
      await fs.mkdir(path.join(baseDir, opts.name), { recursive: true });
      if (opts.verbose) console.log(`作成しました: ${opts.name}`);
      break;
    }
    case "delete": {
      if (!opts.name) { console.error("name が必要です"); process.exit(1); }
      if (!opts.force) {
        console.error("削除には --force が必要です");
        process.exit(1);
      }
      await fs.rm(path.join(baseDir, opts.name), { recursive: true, force: true });
      if (opts.verbose) console.log(`削除しました: ${opts.name}`);
      break;
    }
    case "list": {
      const entries = await fs.readdir(baseDir).catch(() => [] as string[]);
      entries.forEach(e => console.log(e));
      break;
    }
  }
}

const opts = parseArgs(process.argv.slice(2));
run(opts).catch(err => {
  console.error("エラー:", (err as Error).message);
  process.exit(1);
});

実践例2:型安全なHTTPサーバー(素のhttpモジュール)

http モジュールを使ったシンプルなJSON APIサーバーです。ルーティングとリクエストボディのパースを型安全に実装します。

import http from "http";

interface ApiResponse<T = unknown> {
  data?:  T;
  error?: string;
}

// リクエストボディをJSONとして読み取る
function readBody(req: http.IncomingMessage): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    req.on("data", (chunk: Buffer) => chunks.push(chunk));
    req.on("end", () => {
      try {
        const body = Buffer.concat(chunks).toString("utf-8");
        resolve(body ? JSON.parse(body) : {});
      } catch {
        reject(new Error("JSONのパースに失敗しました"));
      }
    });
    req.on("error", reject);
  });
}

function sendJson<T>(res: http.ServerResponse, statusCode: number, body: ApiResponse<T>): void {
  res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
  res.end(JSON.stringify(body));
}

const users: Map<number, { id: number; name: string }> = new Map([
  [1, { id: 1, name: "Alice" }],
  [2, { id: 2, name: "Bob" }],
]);

const server = http.createServer(async (req, res) => {
  const url    = new URL(req.url ?? "/", `http://${req.headers.host}`);
  const method = req.method ?? "GET";

  try {
    if (url.pathname === "/users" && method === "GET") {
      sendJson(res, 200, { data: [...users.values()] });
    } else if (url.pathname.startsWith("/users/") && method === "GET") {
      const id   = parseInt(url.pathname.split("/")[2] ?? "", 10);
      const user = users.get(id);
      if (!user) { sendJson(res, 404, { error: "ユーザーが見つかりません" }); return; }
      sendJson(res, 200, { data: user });
    } else {
      sendJson(res, 404, { error: "Not Found" });
    }
  } catch (e) {
    sendJson(res, 500, { error: (e instanceof Error) ? e.message : "Internal Server Error" });
  }
});

server.listen(3000, () => console.log("http://localhost:3000"));

実践例3:ファイル処理バッチスクリプト

指定ディレクトリ内のJSONファイルを読み込んでバリデーションし、結果をCSVに出力するバッチスクリプトです。fs.promisespath・型安全なJSONパースを組み合わせます。

import { promises as fs } from "fs";
import path from "path";

interface UserRecord {
  id:    number;
  name:  string;
  email: string;
  age:   number;
}

// 型ガード: JSONがUserRecordかを確認
function isUserRecord(v: unknown): v is UserRecord {
  return (
    typeof v === "object" && v !== null &&
    "id"    in v && typeof (v as UserRecord).id    === "number" &&
    "name"  in v && typeof (v as UserRecord).name  === "string" &&
    "email" in v && typeof (v as UserRecord).email === "string" &&
    "age"   in v && typeof (v as UserRecord).age   === "number"
  );
}

async function processDirectory(inputDir: string, outputFile: string): Promise<void> {
  const files = await fs.readdir(inputDir);
  const jsonFiles = files.filter(f => f.endsWith(".json"));

  const rows: string[] = ["id,name,email,age"];  // CSVヘッダー
  let successCount = 0;
  let errorCount   = 0;

  for (const file of jsonFiles) {
    const filePath = path.join(inputDir, file);
    try {
      const raw  = await fs.readFile(filePath, "utf-8");
      const data: unknown = JSON.parse(raw);

      if (!isUserRecord(data)) {
        console.warn(`[SKIP] ${file}: UserRecord 形式ではありません`);
        errorCount++;
        continue;
      }

      // data は UserRecord 型に絞り込まれている
      const escaped = (s: string) => `"${s.replace(/"/g, '""')}"`;
      rows.push(`${data.id},${escaped(data.name)},${escaped(data.email)},${data.age}`);
      successCount++;
    } catch (e) {
      console.error(`[ERROR] ${file}:`, (e instanceof Error) ? e.message : e);
      errorCount++;
    }
  }

  await fs.writeFile(outputFile, rows.join("\n"), "utf-8");

  console.log(`完了: ${successCount}件成功, ${errorCount}件スキップ/エラー`);
  console.log(`出力: ${outputFile}`);
}

processDirectory("./data", "./output/users.csv")
  .catch(err => {
    console.error("致命的エラー:", (err as Error).message);
    process.exit(1);
  });

9. まとめ:Node.js + TypeScript チートシート

やりたいこと コマンド / 設定
初期セットアップ npm install -D typescript @types/node
tsconfig生成 npx tsc --init
開発実行(高速) npx tsx watch src/index.ts
本番ビルド npx tscnode dist/index.js
型チェックのみ npx tsc --noEmit
ts-node高速実行 npx ts-node --transpile-only src/index.ts
ESM有効化 package.json"type": "module"、tsconfig の module: "NodeNext"
__dirname(ESM) fileURLToPath(import.meta.url) + path.dirname()
環境変数を型安全に src/env.d.tsProcessEnv インターフェースを拡張
Expressの型 npm install -D @types/express

FAQ

Qts-node と tsx はどちらを使えばよいですか?

A新規プロジェクトには tsx を推奨します。esbuildベースで起動が速く、設定が少なく、ESM・CommonJS 両方に対応しています。ただし tsx は型チェックをスキップするため、別途 npx tsc --noEmit で型チェックを行ってください。既存のts-nodeプロジェクトはそのまま使い続けても問題ありません。

QNode.jsのバージョンに合ったtargetはどう決めますか?

ANode.js 18以降を使っている場合は "target": "ES2022" が安全です。Node.jsの各バージョンがサポートするECMAScript機能はnode.greenで確認できます。ES2022 には at()Object.hasOwn()Error.cause 等が含まれます。

QCommonJS と ESM、どちらを選べばよいですか?

A既存のnpmパッケージやライブラリとの互換性を重視するなら CommonJS、モダンな構成で新規プロジェクトを始めるなら ESM を選べます。ただし ESM は __dirname が使えない・拡張子の明示が必要など学習コストがあります。迷ったら CommonJS + esModuleInterop: true から始めることをおすすめします。

Qprocess.env.FOOstring | undefined になって困っています。

Asrc/env.d.tsdeclare namespace NodeJS { interface ProcessEnv { FOO: string; } } を定義すると、process.env.FOO の型が string になります(undefinedが消える)。ただし実際に設定されていない場合の安全性は失われるため、アプリ起動時に getEnv("FOO") で存在確認することも合わせて行うと安全です。

Qtsconfig.jsonのlibtypesの違いは何ですか?

Alib はTypeScriptが組み込みで提供する型定義(DOM・ES2022等)の選択、typesnode_modules/@types/ からどのパッケージを読み込むかの選択です。Node.jsアプリでは "lib": ["ES2022"](DOMは不要)、"types": ["node"](@types/nodeのみ)が推奨設定です。tsconfig.json全般の詳細は tsconfig.json 完全ガイド も参照してください。

QExpressのrouterを別ファイルに分ける場合の型定義は?

Aimport { Router } from "express"Router 型を使います。const router = Router(); で作成し、router.get("/path", handler) で登録して、export default router でエクスポートします。ハンドラ関数は (req: Request, res: Response) => void の型を付けると型安全になります。