CLOVER🍀

That was when it all began.

TypeScript 5.0からtsconfig.jsonのextends元を複数指定できるようになっていたという話(+showConfigで最終結果確認)

これは、なにをしたくて書いたもの?

前に、tsconfig.jsonextendsで拡張(オーバーライド)できるらしいというエントリーを書きました。

tsconfig.jsonをextendsして、設定内容をオーバーライドする - CLOVER🍀

あるきっかけで、extendsに指定する対象を複数にできないのかな?と思ったのですが、どうやらTypeScript 5.0でできるようになっていた
みたいです。

tsconifg.jsonに複数のファイルをextends元として指定する

tsconfig.jsonのリファレンスのextendsの部分を見てみます。

Intro to the TSConfig Reference / Root Fields / Extends - extends

ここを見ると、extendsは文字列を指定するオプションに見えるのですが。

TypeScript 5.0のリリースブログを見ると、extendsに複数ファイルを指定できるようになったことが書かれています。

Announcing TypeScript 5.0 - TypeScript

こちらですね。

Announcing TypeScript 5.0 / Supporting Multiple Configuration Files in extends

複数ファイルのextendsができるようになったと書かれています。

To give some more flexibility here, Typescript 5.0 now allows the extends field to take multiple entries.

このように、配列で複数指定できるみたいですね。

{
    "extends": ["a", "b", "c"],
    "compilerOptions": {
        // ...
    }
}

こういう機能があると、「同じフィールドが競合した場合はどうなるのか?」というのが気になるところですが、その場合は後ろに指定したものが
優先されるようです。

Writing this is kind of like extending c directly, where c extends b, and b extends a. If any fields "conflict", the latter entry wins.

それはそうでしょうね、という感じではあります。

tsconfig.jsonのリファレンスには書かれていないのですが、1度試しておこうと思います。

環境

今回の環境はこちら。

$ node --version
v20.16.0


$ npm --version
10.8.1

準備

ひとまず、プロジェクトの作成とTypeScriptのインストールを行います。

$ npm init -y
$ npm i -D typescript
$ npm i -D @types/node@v20

TypeScript 5.5.4です。

  "devDependencies": {
    "@types/node": "^20.14.8",
    "typescript": "^5.5.4"
  }

複数ファイルのextendsを試してみる

それでは、複数ファイルのextendsを試していってみましょう。

まずは単一のファイルをextendsから。

ベースのファイルを用意。

tsconfig.a.json

{
"compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "moduleResolution": "nodenext"
  }
}

これをextendsしたファイルを用意。

tsconfig.last.json

{
  "extends": "./tsconfig.a"
  "compilerOptions": {
    "baseUrl": "./src",
    "outDir": "dist",
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

tscは入力ファイルがなにもないとエラーになってノイズになるので、適当にファイルを用意しておきます。

src/message.ts

export function message(word: string): string {
  return `Hello ${word}!!`;
}

--showConfigオプションで結果を確認。

$ npx tsc --showConfig --project ./tsconfig.last.json

こうなりました。

{
    "compilerOptions": {
        "target": "esnext",
        "module": "nodenext",
        "moduleResolution": "nodenext",
        "baseUrl": "./src",
        "outDir": "./dist",
        "skipLibCheck": true,
        "esModuleInterop": true,
        "moduleDetection": "force",
        "allowSyntheticDefaultImports": true,
        "resolvePackageJsonExports": true,
        "resolvePackageJsonImports": true,
        "useDefineForClassFields": true
    },
    "files": [
        "./src/message.ts"
    ],
    "include": [
        "src"
    ],
    "exclude": [
        "/path/to/dist"
    ]
}

resolvePackageJsonExportsuseDefineForClassFieldsなど、指定していないものも入っているようですが、これはmoduleResolution
targetに指定した値で暗黙的に変わるものらしいです。

つまり、このような変更を含めた最終結果を-showConfigフラグでは確認できるということですね。

--showConfig

Print the final configuration instead of building.

TypeScript: Documentation - tsc CLI Options

さて、今回のお題は複数のextendsでした。というわけでファイルを追加します。

tsconfig.b.json

{
  "compilerOptions": {
    "strict": true
  }
}

tsconfig.c.json

{
  "compilerOptions": {
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true
  }
}

複数のファイルをextends

tsconfig.last.json

{
  "extends": [
    "./tsconfig.a",
    "./tsconfig.b",
    "./tsconfig.c"
  ],
  "compilerOptions": {
    "baseUrl": "./src",
    "outDir": "dist",
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

結果。

$ npx tsc --showConfig --project ./tsconfig.last.json
{
    "compilerOptions": {
        "target": "esnext",
        "module": "nodenext",
        "moduleResolution": "nodenext",
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitOverride": true,
        "noImplicitReturns": true,
        "baseUrl": "./src",
        "outDir": "./dist",
        "skipLibCheck": true,
        "esModuleInterop": true,
        "moduleDetection": "force",
        "allowSyntheticDefaultImports": true,
        "resolvePackageJsonExports": true,
        "resolvePackageJsonImports": true,
        "useDefineForClassFields": true,
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "strictBindCallApply": true,
        "strictPropertyInitialization": true,
        "alwaysStrict": true,
        "useUnknownInCatchVariables": true
    },
    "files": [
        "./src/message.ts"
    ],
    "include": [
        "src"
    ],
    "exclude": [
        "/path/to/dist"
    ]
}

なんかものすごく増えましたが…stricttrueにしたことで有効なものになったものなども展開されているからですね。

では、ここで競合するルールをひとつ追加しましょう。

tsconfig.d.json

{
  "compilerOptions": {
    "forceConsistentCasingInFileNames": false
  }
}

tsconfig.c.jsonと競合する設定です。こちらをextendsに追加。

tsconfig.last.json

{
  "extends": [
    "./tsconfig.a",
    "./tsconfig.b",
    "./tsconfig.c",
    "./tsconfig.d"
  ],
  "compilerOptions": {
    "baseUrl": "./src",
    "outDir": "dist",
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

forceConsistentCasingInFileNamesが上書きされました。

$ npx tsc --showConfig --project ./tsconfig.last.json
{
    "compilerOptions": {
        "target": "esnext",
        "module": "nodenext",
        "moduleResolution": "nodenext",
        "strict": true,
        "forceConsistentCasingInFileNames": false,
        "noFallthroughCasesInSwitch": true,
        "noImplicitOverride": true,
        "noImplicitReturns": true,
        "baseUrl": "./src",
        "outDir": "./dist",
        "skipLibCheck": true,
        "esModuleInterop": true,
        "moduleDetection": "force",
        "allowSyntheticDefaultImports": true,
        "resolvePackageJsonExports": true,
        "resolvePackageJsonImports": true,
        "useDefineForClassFields": true,
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "strictBindCallApply": true,
        "strictPropertyInitialization": true,
        "alwaysStrict": true,
        "useUnknownInCatchVariables": true
    },
    "files": [
        "./src/message.ts"
    ],
    "include": [
        "src"
    ],
    "exclude": [
        "/path/to/dist"
    ]
}

ここですね。

        "forceConsistentCasingInFileNames": false,

extendsの順番を入れ替えてみます。tsconfig.d.jsontsconfig.c.jsonの前にしました。

tsconfig.last.json

{
  "extends": [
    "./tsconfig.a",
    "./tsconfig.b",
    "./tsconfig.d",
    "./tsconfig.c"
  ],
  "compilerOptions": {
    "baseUrl": "./src",
    "outDir": "dist",
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}
$ npx tsc --showConfig --project ./tsconfig.last.json
{
    "compilerOptions": {
        "target": "esnext",
        "module": "nodenext",
        "moduleResolution": "nodenext",
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitOverride": true,
        "noImplicitReturns": true,
        "baseUrl": "./src",
        "outDir": "./dist",
        "skipLibCheck": true,
        "esModuleInterop": true,
        "moduleDetection": "force",
        "allowSyntheticDefaultImports": true,
        "resolvePackageJsonExports": true,
        "resolvePackageJsonImports": true,
        "useDefineForClassFields": true,
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "strictBindCallApply": true,
        "strictPropertyInitialization": true,
        "alwaysStrict": true,
        "useUnknownInCatchVariables": true
    },
    "files": [
        "./src/message.ts"
    ],
    "include": [
        "src"
    ],
    "exclude": [
        "/path/to/dist"
    ]
}

forceConsistentCasingInFileNamestsconfig.c.jsonの値になりました。

        "forceConsistentCasingInFileNames": true,

というわけで、複数ファイルをextendsをした場合の結果と--showConfigフラグについて見てみました。

ちなみに、ここまでずっと--projectフラグでtsconfig.jsonを明示的に指定していましたが、ファイル名がtsconfig.jsonであれば
--projectフラグ自体要りません。

tsconfig.json

{
  "extends": [
    "./tsconfig.a",
    "./tsconfig.b",
    "./tsconfig.c"
  ],
  "compilerOptions": {
    "baseUrl": "./src",
    "outDir": "dist",
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

結果。

$ npx tsc --showConfig
{
    "compilerOptions": {
        "target": "esnext",
        "module": "nodenext",
        "moduleResolution": "nodenext",
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitOverride": true,
        "noImplicitReturns": true,
        "baseUrl": "./src",
        "outDir": "./dist",
        "skipLibCheck": true,
        "esModuleInterop": true,
        "moduleDetection": "force",
        "allowSyntheticDefaultImports": true,
        "resolvePackageJsonExports": true,
        "resolvePackageJsonImports": true,
        "useDefineForClassFields": true,
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "strictBindCallApply": true,
        "strictPropertyInitialization": true,
        "alwaysStrict": true,
        "useUnknownInCatchVariables": true
    },
    "files": [
        "./src/message.ts"
    ],
    "include": [
        "src"
    ],
    "exclude": [
        "/path/to/dist"
    ]
}

まあ、コマンド例、ということで。

おわりに

TypeScript 5.0からtsconfig.jsonのextends元を複数指定できるようになっていたようなので、ちょっと試してみました。

挙動自体はわかりやすかったですが、リファレンスに載っていないのでちょっと???という感じですね。あと、そんなに多く使う機能でも
ないような気はします。

せっかくなので覚えておきましょう、ということで。

--showConfigフラグの方が重要かもしれません。