CLOVER🍀

That was when it all began.

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

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

tsconfig.jsonは、extendsで拡張(というかオーバーライド)できるらしく。

ちょっとやりたいことがあったので、試してみました。

tsconfig.jsonのextends

tsconfig.jsonは、extendsで拡張することができます。

What is a tsconfig.json / TSConfig Bases

extendsで指定したファイルをベースにして、オーバーライドすることができます。ファイル内に記載されているパスは、元のファイルを
基準にして解決するようです。ファイルの循環参照はできません。

The configuration from the base file are loaded first, then overridden by those in the inheriting config file. All relative paths found in the configuration file will be resolved relative to the configuration file they originated in.

It’s worth noting that files, include and exclude from the inheriting config file overwrite those from the base config file, and that circularity between configuration files is not allowed.

Intro to the TSConfig ReferenceCompiler Options / Extends - extends

とりあえず、やってみます。

環境

今回の環境は、こちらです。

$ node --version
v16.13.1


$ npm --version
8.1.2

サンプルプロジェクト作成

確認用の、npmプロジェクトを作成します。

Jestも使うことにします。

$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ npm i -D @types/node@v16
$ npm i -D jest @types/jest
$ npm i -D esbuild-jest esbuild

依存関係。

  "devDependencies": {
    "@types/jest": "^27.4.0",
    "@types/node": "^16.11.19",
    "esbuild": "^0.14.11",
    "esbuild-jest": "^0.5.0",
    "jest": "^27.4.7",
    "prettier": "2.5.1",
    "typescript": "^4.5.4"
  }

Jestの設定。

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "esbuild-jest"
  }
};

.prettierrc.json

{
  "singleQuote": true
}

tsconfig.jsonは、あとで載せます。

ソースコード、テストコードを配置するディレクトリを作成。

$ mkdir src test

ソースコード

src/message.ts

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

テストコード。

test/message.test.ts

import { message } from '../src/message';

test('message test', () => {
  expect(message('World')).toBe('Hello World!!');
});

ソースコード、テストコードはこういう

$ tree src test
src
└── message.ts
test
└── message.test.ts

0 directories, 2 files

確認。

$ npx jest
 PASS  test/message.test.ts
  ✓ message test (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.249 s, estimated 1 s
Ran all test suites.

tsconfig.jsonを拡張する

ここまでtsconfig.jsonを載せていませんでしたが、こういう内容にしていました。こちらを「ベース」にします。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

このベースのファイルを拡張した、もうひとつのファイルを作成。extendsにベースのファイルパスを指定するのですが、拡張子は要らない
みたいですね。

tsconfig-typecheck-only.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src", "test"
  ]
}

差はnoEmittrueにしているのと、includeです。

こちらのファイルではベースのtsconfig.jsonを元に、対象とするディレクトリをsrctestの2つにして、コンパイル結果は出力しないように
しています。

動作確認

ベースのtsconfig.jsonの方では、ビルドするのはsrc`ディレクトリのみを対象にしています。

なので、ビルドすると

$ npx tsc --project .

こうなります。

$ tree dist
dist
└── message.js

0 directories, 1 file

1度、ディレクトリを削除。

$ rm -rf dist

次は、ファイルを切り替えて実行。

$ npx tsc --project ./tsconfig-typecheck-only.json

ビルドが終わっても、

$ ll dist
ls: 'dist' にアクセスできません: そのようなファイルやディレクトリはありません

結果ファイルは生成されません。

ただ、これだとベースを継承しているのかわからないので、もう少し確認してみましょう。

ベースのtsconfig.jsonでは、stricttrueになっています。

    "strict": true,

strictにはstrictNullCheckstrueにする効果も含まれているので、たとえばテストコードを以下のように変更すると

test('message test', () => {
  //expect(message('World')).toBe('Hello World!!');
  expect(message(null)).toBe('Hello World!!');
});

ビルドが通らなくなります。

$ npx tsc --project ./tsconfig-typecheck-only.json
test/message.test.ts:6:18 - error TS2345: Argument of type 'null' is not assignable to parameter of type 'string'.

6   expect(message(null)).toBe('Hello World!!');
                   ~~~~


Found 1 error.

ここで、ベースのtsconfig.jsonstrictfalseに変更してみます。

    "strict": false,

するとビルドが通るようになり、extendsを使用しているファイルがベースのtsconfig.jsonの内容を引き継いでいることが確認できます。

$ npx tsc --project ./tsconfig-typecheck-only.json

そもそもやりたかったこと

前にJestをTypeScriptで扱う際に、速度を改善したいというエントリーを書きました。

Jest+TypeScriptを高速に実行したい(ts-jest、esbuild、SWCを比べる) - CLOVER🍀

この時にesbuildやSWCを使って確認したのですが、これらはTypeScriptを扱うのが高速になりますが、代わりに型チェックを行わなく
なります。

よって、noEmittrueにした状態でテストコードの型チェックを行いたいのですが、ベースのtsconfig.jsonはそのままに差分だけ
変えられないかなぁと思ってこの方法に行き着きました。

tscコマンドラインオプションでも指定できるのですが、他にも変えたい部分もありましたし。

TypeScript: Documentation - tsc CLI Options

package.jsonscriptsは、こんな感じにして型チェックのみ行う定義も使おうかなと。

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "typecheck": "tsc --project ./tsconfig-typecheck-only.json",
    "typecheck:watch": "tsc --project ./tsconfig-typecheck-only.json --watch",
    "format": "prettier --write src",
    "test": "jest"
  },

実際、こんな感じに型が合わない記述をしても

test('message test', () => {
  //expect(message('World')).toBe('Hello World!!');
  expect(message(3)).toBe('Hello World!!');
});

型チェックをすり抜けてしまいますし。

$ npx jest
 FAIL  test/message.test.ts
  ● message test

    expect(received).toBe(expected) // Object.is equality

    Expected: "Hello World!!"
    Received: "Hello 3!!"

      1 | import { message } from '../src/message';
      2 |
    > 3 | test('message test', () => {
        |                                          ^
      4 |   //expect(message('World')).toBe('Hello World!!');
      5 |   expect(message(3)).toBe('Hello World!!');

      at Object.<anonymous> (test/message.test.ts:3:42)

 PASS  dist/test/message.test.js

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.404 s, estimated 1 s
Ran all test suites.

もちろん、tscでのビルドではこれは通りません。

$ npx tsc --project ./tsconfig-typecheck-only.json
test/message.test.ts:5:18 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

5   expect(message(3)).toBe('Hello World!!');
                   ~


Found 1 error.

ちなみに、拡張したファイルからnoEmitを削除すると

tsconfig-typecheck-only.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
  },
  "include": [
    "src", "test"
  ]
}

こういう感じでdistディレクトリ内の階層が変わるのですが。

$ npx tsc --project ./tsconfig-typecheck-only.json
$ tree dist
dist
├── src
│   └── message.js
└── test
    └── message.test.js

2 directories, 2 files

まあ、Node.jsアプリケーションとしてビルドしたい場合は、テストコードのビルド結果が含まれていなくてもいいかなぁと思うので、
こんな感じでいきましょう。