🐙

tsconfig.json の isolatedModules

に公開

概要

TypeScript を JavaScript にトランスパイルする際、複数のファイルを関連付ける必要があります。しかし、Babel/SWC のようなトランスパイラは 1 ファイルずつ処理するため、全体の型システムの知識に依存したコード変換はできません。isolatedModulestrue に設定することで、正しく解釈ができない記述について警告が出るようになります。一例として、declare const enum や型の再 export の警告が出ます。その他の事例は公式ドキュメントを参照ください。

https://www.typescriptlang.org/ja/tsconfig#isolatedModules

Next.js の設定考察

create next-app で作成した Next.js プロジェクトで isolatedModulestrue が設定されています。Next.js では、isolatedModules を無効化しても next build あるいは next dev の実行時に強制的に有効化されます。よって Next.js では isolatedModules を有効化しておきます。

tsconfig.json
{
  "compilerOptions": {
    "isolatedModules": true,
  },
}

参考

公式の説明はこちらです。

https://www.typescriptlang.org/ja/tsconfig#isolatedModules

以下が作業リポジトリです。

https://github.com/hayato94087/tsconfig-isolatedmodules

https://github.com/hayato94087/next-tsconfig-isolatedmodules

この記事の内容

この記事では isolatedModulesfalse の場合と true の場合で動作を確認します。Node.js & TypeScript のプロジェクトと Next.js のプロジェクトで動作確認を行います。

Node.js & TypeScriptのプロジェクトで動作確認

TypeScript の簡易プロジェクトを作成し、isolatedModulesfalse の場合と true の場合で動作を確認します。

事前環境の構築

動作を作業するための Node.js & TypeScript のプロジェクトを作成します。長いので、折り畳んでおきます。

新規プロジェクト作成と初期環境構築の手順詳細

TypeScript の簡易プロジェクトを作成します。

まず、package.json を作成します。

$ mkdir -p tsconfig-isolatedmodules
$ cd tsconfig-isolatedmodules
$ pnpm init

下記で package.json を上書きします。ポイントは scripts に 3 つのスクリプトを追加しています。typecheck で型をチェックし、dev でローカルで動作確認、build でトランスパイルします。

package.json
{
  "name": "tsconfig-isolatedmodules",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "typecheck": "tsc --noEmit",
    "dev": "ts-node index.ts",
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

TypeScript をインストールします。

$ pnpm install -D typescript ts-node

tsconfig.json を作成します。

$ npx tsc --init

tsconfig.json を作成します。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2015",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop":true
  },
  "include": ["**/*.ts","**/*.js"],
  "exclude": ["node_modules", "dist"]
}

git を初期化します。

$ git init

.gitignore を作成します。

$ touch .gitignore
.gitignore
node_modules

コミットします。

$ git add .
$ git commit -m "feat:初期コミット"

isolatedModulesfalse の場合

isolatedModulesfalse の場合の動作を確認します。

tsconfig.jsonisolatedModules を無効化します。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2015",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "skipLibCheck": true,
+   "isolatedModules": false,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop":true
  },
  "include": ["**/*.ts","**/*.js"],
  "exclude": ["node_modules", "dist"]
}

コードを作成します。

$ touch types.ts re-export.ts index.ts
types.ts
export type User = {
  name: string;
  age: number;
};
re-export.ts
export { User } from './types';
index.ts
import { User } from './re-export';

const user: User = {
  name: 'John',
  age: 30,
};

console.log(user);

型をチェックします。エラーは起きません。

$ pnpm run typecheck

ローカルで動作確認します。

$ pnpm run dev

{ name: 'John', age: 30 }

ビルドします。エラーは起きません。

$ pnpm run build

コミットします。

$ git add .
$ git commit -m "feat:isolatedModulesを無効化した場合の挙動を確認"

isolatedModulestrue の場合

isolatedModulestrue の場合の動作を確認します。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2015",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "skipLibCheck": true,
-   "isolatedModules": false,
+   "isolatedModules": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop":true
  },
  "include": ["**/*.ts","**/*.js"],
  "exclude": ["node_modules", "dist"]
}

型チェックします。再 export に関してエラーがでます。

$ pnpm run typecheck

re-export.ts:1:10 - error TS1205: Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'.

1 export { User } from './types';
           ~~~~

Found 1 error in re-export.ts:1

エラーを修正します。

re-export.ts
-export { User } from './types';
+export { type User } from './types';

ローカルで動作を確認します。

$ pnpm run dev                                                           ok  at 05:36:58 

{ name: 'John', age: 30 }

コミットします。

$ git add .
$ git commit -m "feat:isolatedModulesを有効化した場合の挙動を確認"

Next.jsのプロジェクトで動作確認

Next.js のプロジェクトを作成し、isolatedModulestrue の場合と false の場合で動作を確認します。

事前環境の構築

動作を作業するための Next.js プロジェクトを作成します。長いので、折り畳んでおきます。

新規プロジェクト作成と初期環境構築の手順詳細

プロジェクトを作成

create next-app@latestでプロジェクトを作成します。

$ pnpm create next-app@latest next-tsconfig-isolatedmodules --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd next-tsconfig-isolatedmodules

Peer Dependenciesの警告を解消

Peer dependenciesの警告が出ている場合は、pnpm installを実行し、警告を解消します。

 WARN  Issues with peer dependencies found
.
├─┬ autoprefixer 10.0.1
 └── unmet peer postcss@^8.1.0: found 8.0.0
├─┬ tailwindcss 3.3.0
 ├── unmet peer postcss@^8.0.9: found 8.0.0
 ├─┬ postcss-js 4.0.1
 └── unmet peer postcss@^8.4.21: found 8.0.0
 ├─┬ postcss-load-config 3.1.4
 └── unmet peer postcss@>=8.0.9: found 8.0.0
 └─┬ postcss-nested 6.0.0
   └── unmet peer postcss@^8.2.14: found 8.0.0
└─┬ next 14.0.4
  ├── unmet peer react@^18.2.0: found 18.0.0
  └── unmet peer react-dom@^18.2.0: found 18.0.0

以下を実行することで警告が解消されます。

$ pnpm i -D postcss@latest react@^18.2.0 react-dom@^18.2.0

不要な設定を削除し、プロジェクトを初期化します。

styles

CSSなどを管理するstylesディレクトリを作成します。globals.cssを移動します。

$ mkdir -p src/styles
$ mv src/app/globals.css src/styles/globals.css

globals.cssの内容を以下のように上書きします。

src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

初期ページ

app/page.tsxを上書きします。

src/app/page.tsx
import { type FC } from "react";

const Home: FC = () => {
  return (
    <div className="">
      <div className="text-lg font-bold">Home</div>
      <div>
        <span className="text-blue-500">Hello</span>
        <span className="text-red-500">World</span>
      </div>
    </div>
  );
};

export default Home;

レイアウト

app/layout.tsxを上書きします。

src/app/layout.tsx
import "@/styles/globals.css";
import { type FC } from "react";
type RootLayoutProps = {
  children: React.ReactNode;
};

export const metadata = {
  title: "Sample",
  description: "Generated by create next app",
};

const RootLayout: FC<RootLayoutProps> = (props) => {
  return (
    <html lang="ja">
      <body className="">{props.children}</body>
    </html>
  );
};

export default RootLayout;

TailwindCSSの設定

TailwindCSSの設定(tailwind.config.ts)を上書きします。

tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  plugins: [],
}
export default config

TypeScriptの設定

TypeScriptの初期設定はこちらです。

tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

スクリプトを追加

型チェックのスクリプトを追加します。

package.json
{
  "name": "next-tsconfig-isolatedmodules",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
+   "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "next": "14.1.4"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.0.1",
    "eslint": "^8",
    "eslint-config-next": "14.1.4",
    "postcss": "^8.4.38",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "tailwindcss": "^3.3.0",
    "typescript": "^5"
  }
}

動作確認

ローカルで動作確認します。

$ pnpm run dev

コミットして作業結果を保存しておきます。

$ git add .
$ git commit -m "feat:新規にプロジェクトを作成し, 作業環境を構築"

isolatedModulesfalse の場合

isolatedModulesfalse の場合の動作を確認します。

tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
-   "isolatedModules": true,
+   "isolatedModules": false,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

incrementaltrue の場合、設定変更がうまく反映されないため tsconfig.tsbuildinfo を削除します。

$ rm -rf tsconfig.tsbuildinfo

コードを作成します。

$ mkdir src/lib
$ touch src/lib/types.ts src/lib/re-export.ts
src/lib/types.ts
export type User = {
  name: string;
  age: number;
};
src/lib/re-export.ts
export { User } from './types';

再 export を含んだコードを作成します。

src/app/page.tsx
import { type FC } from "react";
+import { User } from "@/lib/re-export";

+const user: User = {
+  name: "John",
+  age: 30,
+};

const Home: FC = () => {
+  console.log(user);

  return (
    <div className="">
      <div className="text-lg font-bold">Home</div>
      <div>
        <span className="text-blue-500">Hello</span>
        <span className="text-red-500">World</span>
      </div>
    </div>
  );
};

export default Home;

型をチェックします。エラーは起きません。

$ pnpm run typecheck

ローカルで動作確認します。無事動作します。

$ pnpm run dev

 Next.js 14.1.4
   - Local:        http://localhost:3000


   We detected TypeScript in your project and reconfigured your tsconfig.json file for you. Strict-mode is set to false by default.
   The following mandatory changes were made to your tsconfig.json:

        - isolatedModules was set to true (requirement for SWC / Babel)

 Ready in 2.4s
 Compiling / ...
 Compiled / in 1963ms (455 modules)
{ name: 'John', age: 30 }
 Compiled in 291ms (220 modules)

next dev 実行時のログにある通り、強制的に isolatedModules が有効化されます。

   We detected TypeScript in your project and reconfigured your tsconfig.json file for you. Strict-mode is set to false by default.
   The following mandatory changes were made to your tsconfig.json:

        - isolatedModules was set to true (requirement for SWC / Babel)

改めて isolatedModulesfalse にします。

tsconfig.json
{
  "compilerOptions": {
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
-   "isolatedModules": true,
+   "isolatedModules": false,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

incrementaltrue の場合、設定変更がうまく反映されないため tsconfig.tsbuildinfo を削除します。

$ rm -rf tsconfig.tsbuildinfo

ビルドします。

$ pnpm run build

 Next.js 14.1.4

   Creating an optimized production build ...
 Compiled successfully
   Linting and checking validity of types  ...
   We detected TypeScript in your project and reconfigured your tsconfig.json file for you. Strict-mode is set to false by default.
   The following mandatory changes were made to your tsconfig.json:

        - isolatedModules was set to true (requirement for SWC / Babel)

   Linting and checking validity of types  ..Failed to compile.

./src/lib/re-export.ts:1:10
Type error: Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'.

> 1 | export { User } from './types';
    |          ^
  2 |
   Linting and checking validity of types 

next build 実行時のログにある通り、強制的に isolatedModules が有効化されます。

   The following mandatory changes were made to your tsconfig.json:

        - isolatedModules was set to true (requirement for SWC / Babel)

また下記の通りエラーが出ています。

./src/lib/re-export.ts:1:10
Type error: Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'.

> 1 | export { User } from './types';
    |          ^
  2 |
   Linting and checking validity of types 

エラーを修正します。

re-export.ts
-export { User } from './types';
+export { type User } from './types';

ビルドします。エラーは出ません。

$ pnpm run build

 Next.js 14.1.4

   Creating an optimized production build ...
 Compiled successfully
 Linting and checking validity of types    
 Collecting page data    
   Generating static pages (0/5)  [=   ]{ name: 'John', age: 30 }
{ name: 'John', age: 30 }
 Generating static pages (5/5) 
 Collecting build traces    
 Finalizing page optimization    

Route (app)                              Size     First Load JS
 /                                    137 B          84.4 kB
 /_not-found                          885 B          85.2 kB
+ First Load JS shared by all            84.3 kB
 chunks/672-f5652b77b66c42f3.js       29 kB
 chunks/90234aad-523d1b31770fe8fa.js  53.4 kB
 other shared chunks (total)          1.87 kB


  (Static)  prerendered as static content

コミットします。

$ git add .
$ git commit -m "feat:isolatedModulesを無効化した場合の挙動を確認"

Next.jsの設定考察

create next-app で作成した Next.js プロジェクトで isolatedModulestrue が設定されています。Next.js では、isolatedModules を無効化しても next build あるいは next dev の実行時に強制的に有効化されます。よって Next.js では isolatedModules を有効化しておきます。

まとめ

この記事では isolatedModulesfalse の場合と true の場合で動作を確認しました。Node.js & TypeScript のプロジェクトと Next.js のプロジェクトで動作確認を行いました。

参考

https://qiita.com/ryokkkke/items/390647a7c26933940470#isolatedmodules

Discussion