tsconfig.jsonについて調べてみた
TypeScriptの設定ファイルtsconfig.jsonに関する調査メモ
target
- 出力するJavaScriptのバージョンを指定
- デフォルトは、
ES5
- 適切に設定することで
- コンパイル時間が短縮される
- ランタイム時の実行効率が向上する
- 開発時の型チェックが強化される
targetのオプションごとの挙動の違い
targetオプションごとの出力コードの違いを比較
- TypeScriptコード(元のコード)
const fetchData = async (url: string): Promise<{ data: string }> => {
const response = await fetch(url);
return { data: await response.text() };
};
- JavaScriptコード(出力コード)
target: "ES5"
の場合
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var fetchData = function (url) {
return __awaiter(this, void 0, void 0, function () {
var response, text;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [4 /*yield*/, fetch(url)];
case 1:
response = _a.sent();
return [4 /*yield*/, response.text()];
case 2:
text = _a.sent();
return [2 /*return*/, { data: text }];
}
});
});
};
target: "ES2015"
の場合
const fetchData = async (url) => {
const response = await fetch(url);
return { data: await response.text() };
};
target: "ES5"
→ async/awaitがPromiseベースのコードに変換
target: "ES2015"
→ async/await がそのまま利用可能
→ ES2015にコンパイルすると、
- コード量が減り、出力ファイルサイズが小さくなる → コンパイル時間が短縮される
- 最新の構文を利用できるため、JITコンパイラによる最適化が効きやすくなる → ランタイム時の実行効率が向上する
targetオプションごとのlibの違いを比較
target を変更すると、型チェックで使用されるlibのデフォルト値も変わる
target: "ES5"
の場合、libのデフォルト値はlib: ["es5", "dom", "dom.iterable"]
になる
→ Promiseはlibに含まれていないため、以下のコードは型エラーが発生する
const promise: Promise<string> = new Promise(resolve => resolve("Hello")); // エラー: 'Promise' が見つかりません
targetを"ES2015"
の場合、libのデフォルト値はlib: ["es2015", "dom", "dom.iterable"]
になる
→ Promiseはes2015に含まれているため、型エラーは発生しない
最適なtargetの選び方
出力されたJavaScriptコードの実行環境に応じて選択
-
ブラウザ上で実行する場合
- サポート対象の最も古いブラウザバージョンに合わせる
- 例: Chrome 最新版 / Safari 最新版 / Firefox 最新版 →
target: "ES2022"
(対応表を参照)
-
Node.js上で実行する場合
- 本番環境のNode.jsバージョンに合わせる
- 例: Node.js 22 → target: "ES2022"(tsconfig/basesから@tsconfig/node22を参照)
lib
- 型チェック時に参照する標準ライブラリを指定
- targetによってlibのデフォルト値が設定されるが、libを明示的に設定することで、環境に適した型定義を選択できる
libのオプションごとの挙動の違い
"target": "ES2022",
"lib": ["ES2022"] // "target": "ES2022"の時のデフォルト値
の場合、ES2022にfetchは含まれていないため、以下のコードで型エラーが発生する
fetch("https://example.com"); // Cannot find name 'fetch'.
ブラウザAPI(fetchやdocumentなど)を使用する場合は、libにDOMを追加する必要がある
"target": "ES2022",
"lib": ["ES2022", "DOM"]
の場合、fetchはDOMに含まれているため、型エラーは発生しない
最適なlibの選び方
出力されるJavaScriptコードの実行環境に応じて選択
-
ブラウザ上で実行する場合
- fetchやdocumentの型を使えるように"DOM"や"DOM.Iterable"を設定する
- 例:
"target": "ES2022", "lib": ["ES2022", "dom", "dom.iterable"]
-
Node.js上で実行する場合
-
target"ES2022"
の場合でも、なるべく最新のJavaScript仕様を対応するために"ES2024"等を設定する- "ESNext"なら常に最新のJavaScript仕様を反映できるが、TypeScriptのアップデートによる予期しない変更のリスクがあるため、基本的に避けるのが良さそう
- "ES2024"を指定した場合、それ以降のAPIが型チェック上は使用可能となるため、実行時エラーを防ぐために確認し、必要ならポリフィルを導入する
- 例:
"target": "ES2022", "lib": ["ES2024"]
-
module
- 出力されるJavaScriptコードのモジュールシステムを指定
- デフォルトは
-
target: "ES5"
の場合 → CommonJS - 上記以外 → ES6
-
- 適切に設定しないと、環境によってimport/requireの互換性エラーが発生
- ESModules環境でrequireを使う →
SyntaxError: Cannot use require in ES module scope
- CommonJS環境でimportを使う →
SyntaxError: Cannot use import statement outside a module
- ESModules環境でrequireを使う →
moduleのオプションごとの挙動の違い
- TypeScriptコード(元のコード)
// module.ts
export function greet(name: string) {
return `Hello, ${name}!`;
}
// index.ts
import { greet } from "./module";
console.log(greet("Alice"));
- JavaScriptコード(出力コード)
"module": "CommonJS"
の場合
// module.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.greet = void 0;
function greet(name) {
return `Hello, ${name}!`;
}
exports.greet = greet;
// index.js
"use strict";
const { greet } = require("./module");
console.log(greet("Alice"));
"module": "esnext"/"nodenext"
の場合
// module.js
export function greet(name) {
return `Hello, ${name}!`;
}
// index.js
import { greet } from "./module.js";
console.log(greet("Alice"));
最適なmoduleの選び方
-
"esnext"/"nodenext"
(JavaScriptコードがESMで出力されるもの)を選択- ESMにすることで、コードの再利用や管理が効率的に行える
- ブラウザ上で実行する場合 →
esnext
- 最新のECMAScript機能を使い、モダンブラウザでimport/exportをそのまま利用できる
- Node.js上で実行する場合 →
nodenext
- Node.jsで、ESMとCommonJSの両方のモジュール形式を使うことができ、どちらの形式でも正常に動作するように処理できる
moduleResolution
- コンパイル時に、特定のファイル種別をどの順序で見つけるかを指定
- デフォルトは
-
module: "node16"/"nodenext"
の場合 →"node16"/"nodenext"
- 上記以外 →
node
-
- 適切に設定しないと、ファイルや型が正しく見つからず、ビルドや実行時にエラーが発生
Cannot find module 'xxx'
Cannot find module 'xxx' or its corresponding type declarations.
TypeError: xxx is not a function
moduleResolutionのオプションごとの挙動の違い
TypeScriptコード(元のコード)
/project
├── src/
│ ├── index.ts
│ ├── utils.ts
├── node_modules/
│ ├── some-package/
│ │ ├── package.json
│ │ ├── index.js
│ │ ├── index.mjs
│ │ ├── index.cjs
export const helper = () => console.log("Helper function");
{
"main": "./index.js",
"exports": {
"import": "./index.mjs",
"require": "./index.cjs"
}
}
moduleResolution: "node"
の場合
-
モジュールの読み込みの仕組み
- node_modules を探索
- package.json の "main" フィールドを参照
- .d.ts ファイルがあれば型定義として利用
- import時に拡張子の明示は不要(.ts → .js → .d.ts の順で検索)
-
どのファイルが読み込まれるか
import { helper } from "./utils"; // utils.tsを参照
import { someFunction } from "some-package"; // package.jsonの"main"からindex.jsを参照
- 発生しうるエラー
// TypeError: someFunction is not a function
import { someFunction } from "some-package"; // CommonJSをESMとして読み込むと発生
// "main": "./index.js" を参照
// index.jsがmodule.exports = {} の場合、ESMのimportと互換性がなくエラー
moduleResolution: "node16" / "nodenext"
の場合
-
モジュールの読み込みの仕組み
- package.jsonの"exports"を考慮
- 拡張子が必須(例: import "./utils.mjs")
- 拡張子を考慮して .mjs, .cjs, .ts, .js を選択
-
どのファイルが読み込まれるか
import { helper } from "./utils.mjs"; // utils.mjsを参照
import { someFunction } from "some-package"; // package.jsonの"main"からindex.mjsを参照
const { someFunction } = require("some-package"); // package.jsonの"main"からindex.cjsを参照
- 発生しうるエラー
// Cannot find module './utils'
import { helper } from "./utils"; // 拡張子が必須なので、"./utils.mjs" や "./utils.ts" にする必要がある
moduleResolution: "bundler"
の場合
-
モジュールの読み込みの仕組み(バンドラーに依存)
- TypeScriptは型情報のみを処理し、どのファイルを使うかは判断しない
- 相対パスのimportは.ts, .js, .mjs などをバンドラーが探す
- node_modulesの探索やファイルの選択はバンドラーが行う
- node_modulesのimportはバンドラーがpackage.jsonの"exports"やmainFieldsを参照
-
どのファイルが読み込まれるか(Viteの場合)
import { helper } from "./utils"; // .ts, .js, .mjsなどをesbuildが判別して参照
import { someFunction } from "some-package"; // esbuildがpackage.jsonの"exports"やmainFieldsを参照
- どのファイルが読み込まれるか(Webpackの場合)
import { helper } from "./utils"; // .ts, .js, .mjsなどをresolve.extensionsの設定に従って選択
import { someFunction } from "some-package"; // package.jsonの"exports"やmainFieldsを参照
最適なmoduleResolutionの選び方
出力されたJavaScriptコードの実行環境に応じて選択
- ブラウザ上で実行する場合(Vite、Webpackなどのバンドラーを使用) →
bundler
→ バンドラーがESM、CommonJSの違いを吸収する - Node.js上で実行する場合 →
node16
ornodenext
→ ESM / CommonJS の両方を扱える
まとめ
出力されたJavaScriptコードが
- ブラウザ上で実行される場合
{
"target": "ES2023", // 主に最新の主要ブラウザ(Chrome、Edge、Firefox、Safariなど)を想定している場合
"lib": ["ES2023", "DOM", "DOM.Iterable"], // ブラウザAPIの型を参照可能
"module": "esnext", // 最新のモジュールシステムを使用
"moduleResolution": "bundler" // バンドラーに最適化されたモジュール解決方式を採用
}
- Node.js上で実行される場合
{
"target": "ES2023", // Node.js22を対象にしている場合
"lib": ["ES2024"], // ES2024の標準ライブラリの型を参照可能
"module": "nodenext", // Node.jsのESM/CJS 両対応のモジュールシステムを使用
"moduleResolution": "nodenext" // Node.jsのモジュール解決方式を採用
}
参考資料
Discussion