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のインストールとfs・path・processの型定義- 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"
新規プロジェクトでは
dev に tsx watch、build に tsc を使う構成が現在のベストプラクティスです。tsxは型チェックをスキップしてトランスパイルのみ行うため、型エラーは別途 tsc --noEmitでチェックするか、CI/CDに組み込みましょう。2. Node.js 向け tsconfig.json の推奨設定
Node.js用のtsconfig.jsonは、フロントエンド向けとは異なるオプションが必要です。target・module・moduleResolution の設定が特に重要です。
// 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 | ES2019 〜 ES2021 |
古いLTSはES2022の一部機能が未サポート |
3. @types/node の使い方
@types/node はNode.jsの組み込みモジュール(fs・path・http・process 等)の型定義パッケージです。インストールすると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つあります。CommonJS(require/module.exports)は従来形式で、ESM(import/export)はECMAScript標準形式です。TypeScriptで書く場合は常に import/export 構文を使えますが、出力形式は tsconfig.json の module オプションで決まります。
| 比較項目 | 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 を指定
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.ProcessEnv(Record<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 をインストールすると、Request・Response・NextFunction の型が利用できます。
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.json の types に "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.json の module が CommonJS なのに import 構文の出力が require にならない、または逆 |
module と package.json の type フィールドを合わせる |
Type 'string | undefined' is not assignable to type 'string' |
process.env.FOO が string | 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.promises・path・型安全な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 tsc → node 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.ts で ProcessEnv インターフェースを拡張 |
| 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.FOO が string | undefined になって困っています。
Asrc/env.d.ts に declare namespace NodeJS { interface ProcessEnv { FOO: string; } } を定義すると、process.env.FOO の型が string になります(undefinedが消える)。ただし実際に設定されていない場合の安全性は失われるため、アプリ起動時に getEnv("FOO") で存在確認することも合わせて行うと安全です。
Qtsconfig.jsonのlibとtypesの違いは何ですか?
Alib はTypeScriptが組み込みで提供する型定義(DOM・ES2022等)の選択、types は node_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 の型を付けると型安全になります。

