TypeScriptの新規プロジェクトのためのtsconfig設定
1. 概要
tsconfig.jsonファイルは、TypeScriptが型チェックとコードのコンパイルを行う方法を決めます。設定が間違っていると、実行時にインポートエラーが発生したり、コンパイラが多くの型エラーを見落としたりする可能性があります。
この記事では、新しいNode.jsプロジェクト用のtsconfig.jsonをゼロから構築します。厳密な型チェックフラグを有効にし、適切なモジュールシステムを選択し、build設定とtest設定を分離します。読み終わったら、すぐに使える設定セットが得られ、その中の各行の意味を理解できるようになります。
2. プロジェクトの初期化
ディレクトリを作成してTypeScriptをdev dependencyとしてインストールします:
mkdir order-service && cd order-service
npm init -y
npm install --save-dev typescript @types/node
npx tsc --init
tsc --initコマンドは、多くのコメント行を含むtsconfig.jsonを生成します。このファイルは参考資料として役立ちますが、保守が難しいです。代わりに、各オプションを明確に記載した簡潔なバージョンで置き換えます。
次に、package.jsonでプロジェクトがES Moduleを使用することを宣言します:
{
"name": "order-service",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js"
}
}
"type": "module"行は、Node.jsに対して.jsファイルがESMであることを通知します。この選択は、セクション3で見るように、TypeScriptがモジュールを解決する方法に直接影響します。
3. Node.jsの最小設定
以下は基本的なtsconfig.jsonです:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"],
"rootDir": ".",
"outDir": "dist",
"sourceMap": true,
"declaration": false,
"strict": true,
"skipLibCheck": true
},
"include": ["src", "tests"]
}
3.1. target、libとtypes
targetはコンパイラが出力するJavaScriptのバージョンです。ES2022はNode.js 18以上に適しています。このtargetでは、async/await、オプショナルチェーニング、クラスフィールドがそのまま保持され、ダウンレベル化されません。libは実行時に利用可能なAPIを宣言します。"DOM"がないため、サーバーコードで誤ってdocumentやwindowを呼び出すことがありません。types: ["node"]はNode.jsの型だけをロードします。これを指定しないと、TypeScriptはnode_modules/@typesのすべてのパッケージを自動的にロードします。これは時々、望ましくないグローバル型をもたらします。
3.2. moduleとmoduleResolution
Node.jsで直接実行するプロジェクトの場合、両方のオプションにNodeNextを使用します。このモードでは、TypeScriptはNode.jsがインポートを解決する方法を正確にシミュレートします。package.jsonの"type"と"exports"フィールドを読み取り、相対インポートにはファイル拡張子が必須です。
以下の2つのファイルがあると仮定します:
// src/order.ts
export interface Order {
id: string;
items: string[];
note?: string;
}
export function totalItems(order: Order): number {
return order.items.length;
}
// src/index.ts
import { totalItems } from "./order";
console.log(totalItems({ id: "A1", items: ["pho", "tra-da"] }));
npx tsc --noEmitを実行するとエラーが報告されます:
src/index.ts:1:28 - error TS2835: Relative import paths need explicit file extensions
in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'.
Did you mean './order.js'?
import { totalItems } from "./order.js";に修正する必要があります。拡張子は.tsではなく.jsです。これは、実行時にNode.jsがコンパイル済みファイルを読む必要があるためです。この規則は最初は奇妙に見えますが、デプロイ時のERR_MODULE_NOT_FOUNDエラーを避けるのに役立ちます。
3.3. rootDirとoutDir
outDirはビルド後の.jsファイルを保存する場所です。rootDirはoutDir内のディレクトリ構造を決定します。基本設定では、includeにsrcとtestsの両方が含まれるため、rootDirは"."です。セクション6では、ビルド用ファイルがrootDirをsrcに変更して、dist/index.jsがdist/src/index.jsではなく正しい場所に配置されるようにします。
4. Strictモードと追加の検査フラグ
strict: trueフラグは、strictNullChecks、noImplicitAny、strictFunctionTypesを含む一連の検査を有効にします。新しいプロジェクトでは、このフラグを常に有効にすべきです。大きなコードベースに後から有効にすると、はるかに多くの作業が必要になります。
strictに加えて、次のフラグを追加することをお勧めします:
{
"compilerOptions": {
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
これらの行を基本ファイルのcompilerOptionsブロックに追加します。最初の2つのフラグについて詳しく説明する価値があります。
4.1. noUncheckedIndexedAccess
デフォルトでは、TypeScriptはインデックスでアクセスした要素が常に存在すると見なします。しかし、実際はそうではありません:
import type { Order } from "./order.js";
const ordersById: Record<string, Order> = {};
const order = ordersById["A1"];
console.log(order.id);
このフラグを有効にすると、orderの型はOrder | undefinedになり、コンパイラは報告します:
error TS18048: 'order' is possibly 'undefined'.
使用前にチェックする必要があります: if (order) { console.log(order.id); }。フラグを無効にした場合、上記のコードは引き続きコンパイルされますが、実行時にTypeErrorがスローされます。
4.2. exactOptionalPropertyTypes
このフラグは「プロパティがない」と「プロパティにundefinedの値がある」を区別します:
import type { Order } from "./order.js";
const order: Order = { id: "A2", items: [], note: undefined };
error TS2375: Type '{ id: string; items: never[]; note: undefined; }' is not assignable
to type 'Order' with 'exactOptionalPropertyTypes: true'.
この区別は、Object.keys、in演算子、またはJSONへのシリアライズを使用する場合に重要です。実際にundefinedを許可する場合は、明示的に宣言してください: note?: string | undefined。
注意:
exactOptionalPropertyTypesは時々、サードパーティライブラリの型と競合することがあります。特に、フォーム処理とORMライブラリの場合があります。このフラグが制御不能なエラーを多く引き起こす場合は、無効にしてください。ただし、noUncheckedIndexedAccessは保持すべきです。
5. モジュール間の相互作用
次の2つのフラグをcompilerOptionsに追加します:
{
"compilerOptions": {
"verbatimModuleSyntax": true,
"isolatedModules": true
}
}
verbatimModuleSyntaxには単純なルールがあります: typeという単語を含まないインポートはそのままoutputに保持され、typeを含むインポートは削除されます。これにより、コンパイル結果が予測しやすくなります。代わりに、型専用のインポートを明確にマークする必要があります:
// 誤り: Orderはinterfaceのみです
import { Order } from "./order.js";
error TS1484: 'Order' is a type and must be imported using a type-only import
when 'verbatimModuleSyntax' is enabled.
// 正しい
import type { Order } from "./order.js";
import { totalItems } from "./order.js";
isolatedModulesは、各ファイルが独立してコンパイルできることを保証します。esbuild、swc、Vitestなどのツールはそれぞれのファイルを個別に処理するため、このフラグはそれらのツールで問題が発生する前にエラーをキャッチするのに役立ちます。
古いガイドでesModuleInteropを見たことがあるかもしれません。module: NodeNextの場合、このフラグはデフォルトで有効になっているため、追加で宣言する必要がありません。
6. typecheck設定とbuild設定の分離
基本的なtsconfig.jsonファイルはエディタとtypecheckコマンド用です。これにはtestsも含まれます。ビルド時には、testファイルをdistに含めたくありません。したがって、追加のtsconfig.build.jsonを作成します:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"noEmit": false
},
"include": ["src"],
"exclude": ["**/*.test.ts"]
}
extendsは基本ファイルからすべてのcompilerOptionsを継承します。子ファイルでは、includeとexcludeは親ファイルの値を完全に置き換え、マージされません。ビルドコマンドを実行します:
npm run build
ls dist
index.js index.js.map order.js order.js.map
distディレクトリはアプリケーションコードのみを含み、rootDir: "src"のおかげで、すぐにルートに配置されます。
6.1. Vitestでテストを書く
Vitestをインストールします:
npm install --save-dev vitest
scriptsに"test": "vitest run"を追加し、totalItems関数のテストを書きます:
// tests/order.test.ts
import { describe, expect, it } from "vitest";
import type { Order } from "../src/order.js";
import { totalItems } from "../src/order.js";
describe("totalItems", () => {
it("注文のアイテム数を正しくカウントする", () => {
const order: Order = { id: "A1", items: ["pho", "tra-da", "quay"] };
expect(totalItems(order)).toBe(3);
});
it("空の注文に対して0を返す", () => {
expect(totalItems({ id: "A2", items: [] })).toBe(0);
});
});
npm run typecheck && npm test
✓ tests/order.test.ts (2 tests)
Test Files 1 passed (1)
Tests 2 passed (2)
Vitestは実行時に型を自動的に削除しますが、型をチェックしません。したがって、CIでは常にtypecheckを実行してください。そうしないと、型エラーのあるテストでも「pass」する可能性があります。
7. bundlerを使用するプロジェクトのバリエーション
コードがNode.jsで直接実行される代わりにVite、esbuild、またはwebpackでバンドルされている場合、モジュール部分を次のように変更します:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"jsx": "react-jsx"
}
}
moduleResolution: "Bundler"はファイル拡張子なしのインポートを許可します。bundlerが独自に解決するためです。noEmit: trueは、tscが型チェックのみを行い、bundlerがファイルの出力を処理することを意味します。- ブラウザで実行されるコードであるため、
"DOM"を追加します。jsx行はReactを使用する場合のみ必要です。
注意:
node dist/index.jsで直接実行されるコードにBundlerを使用しないでください。コンパイラは.js拡張子のないインポートを受け入れますが、Node.jsは実行時にエラーを報告します。
8. よくあるエラー
pathsを使用してエイリアスを作成し、tscでビルドする。 "paths": { "@/*": ["src/*"] }を宣言したと仮定します。エディタはこのエイリアスを理解しますが、tscはoutput内のパスを書き直さないため、nodeは@/order.jsを見つけることができません。Node.jsプロジェクトの場合、package.jsonでsubpath importsを使用します:
{
"imports": {
"#src/*": "./dist/*"
}
}
その後、import { totalItems } from "#src/order.js";を使ってインポートします。NodeNextモードでは、TypeScriptはimportsフィールドを読み取り、distからsrcへの逆マッピングを行うため、エディタとruntimeの両方が正しく解決されます。
skipLibCheckの過度な使用。 このフラグは.d.tsファイルのチェックのみをスキップし、ビルドを高速化し、ライブラリの型からのエラーを回避するのに役立ちます。これによってコードの安全性が低下することはありません。ただし、.d.tsファイルを自分で書く場合は、これらのファイル自体のエラーもスキップされることに注意してください。
includeを忘れる。 includeがない場合、TypeScriptはディレクトリ全体をスキャンします(distも含む)。結果は通常、エラーTS5055: Cannot write file ... because it would overwrite input fileです。
CommonJSとESMの混在。 package.jsonに"type": "module"がないのにNodeNextを使用する場合、.tsファイルはCommonJSにコンパイルされます。その場合、ESMのみをサポートするパッケージをimportするとエラーが発生します。最初から1つの方向を選択してください。新しいプロジェクトではESMを選択すべきです。
strictを有効にしてからanyを至る所に分散させる。 これはstrict modeを無効にします。データの型がわからない場合は、unknownを使用して、チェック文で型を絞り込みます。
9. 結論
この記事では、新しいNode.jsプロジェクト用のtsconfig.jsonを構築しました。以下の部分が含まれています:
moduleとmoduleResolutionをNodeNextに設定し、"type": "module"を伴わせてコンパイラが実行時を正確にシミュレートします。strictとnoUncheckedIndexedAccessおよびexactOptionalPropertyTypesフラグを使用して、エラーを早期にキャッチします。verbatimModuleSyntaxとisolatedModulesでoutputを予測可能にし、高速コンパイルツールとの互換性を確保します。tsconfig.build.jsonファイルを分離して、distにはアプリケーションコードのみが含まれるようにします。
プロジェクトがbundlerを使用している場合は、moduleResolution: "Bundler"とnoEmit: trueに変更するだけです。厳密な検査フラグは変わりません。最後に、CIで常にtsc --noEmitを実行してください。ほとんどのテスト実行ツールとbundlerは型チェックを行わないためです。