tsconfig の target と lib の違いを具体例で理解する 


## はじめに

TypeScript の `tsconfig.json` では、`target` と `lib` に `"es2015"`, `"es2020"`, `"esnext"` などの ECMAScript バージョンを指定できます。しかし、これらの違いを明確に理解するのは意外と難しく、私自身も混乱したことがありました。個人的には特に `lib` の存在意義がなかなか理解できませんでした。

この記事では、`target` と `lib` の違いをより分かりやすく説明するために、実際に手を動かしながら検証していきます。


## 検証環境の用意

:::note info
特に特殊なことはしてません。自分で用意できる方、あるいは検証する必要がない方は読み飛ばしてください。
:::

今回の検証には **Node.js 16** を使用します。（本当はもっと古いバージョンを使いたかったのですが、セットアップが謎に大変で断念）
Node.js 16 は **ECMAScript 2021** までをサポートしています。

まずは、お手元の環境に Node.js 16 をインストールしてほしいのですが、Docker を使用すれば手軽に準備できます。
検証用のディレクトリを作成し、以下の `Dockerfile` と `devcontainer.json` を用意して、VS Code から devcontainer を開いてください。

```Dockerfile:Dockerfile
FROM node:16.0.0
CMD ["sleep", "infinity"]
```

```json:.devcontainer/devcontainer.json
{
  "name": "Development Container",
  "build": {
    "dockerfile": "../Dockerfile",
    "context": ".."
  }
}
```


Node.js 16 が動作する環境を用意できたら、まずは `package.json` を生成します。
```sh
npm init -y
```

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

```sh
npm install -D typescript
```

最後に、本稿の主役である `tsconfig.json` を生成します。

```sh
npx tsc --init
```

なお、トランスパイル後のファイルの出力先として、`tsconfig.json` の `outDir` は `"./out"` に設定しておきます。

```json:tsconfig.json
{
    "compilerOptions": {
        ...
        "outDir": "./out"
        ...
    }
}
```


## 重要ポイント：target と lib は対象が異なる

違いを理解するうえで一番重要なポイントを最初に抑えておきます。
`target` と `lib` は、それぞれ以下の異なる要素を対象としています。
- `target`: **構文（シンタックス）** に関する設定
- `lib`: **API（標準組み込みオブジェクトやメソッド）** に関する設定

ここで言う構文とは、例えば `const` や `let`、アロー関数 `() => {}`、クラスの `static` ブロックなどが該当します。


一方で、API に関しては `Map` などの標準組み込みオブジェクトや、`Array.at` のようなメソッドが該当します。

## target と構文の関係

`target` は、TypeScript のコードをJavaScriptにトランスパイルする際の **構文の変換ルール** を決定します。

その具体的な例として、クラスの `static` ブロックを取り上げます。 `static` ブロックは ECMAScript 2022 で導入された構文であり、Node.js 16（ECMAScript 2021 までサポート）では使用できません。

実際に、以下のような JavaScript コード `syntax.js` を作成し、 `node syntax.js` で実行してみましょう。ランタイムエラーが発生するはずです。

```js:syntax.js
class Example {
    static var = "foo"
    static {
        this.var = "bar"
    }
}

console.log(Example.var)

```

続いて、この問題が TypeScript のトランスパイルにより解決する様子を見ていきます。

まずは `syntax.ts` を作成し、`syntax.js` と同じコードを記述します。Node.js 16 で動作させるべく、 `tsconfig.json` の `target` を `"es2021"` に設定してトランスパイルしましょう。

```sh
npx tsc
```

出力された `out/syntax.js` を確認すると、`static` ブロックが別の構文に変換されていることがわかります。

```javascript:out/syntax.js
"use strict";
var Example = /** @class */ (function () {
    function Example() {
    }
    var _a;
    _a = Example;
    Example.var = "foo";
    (function () {
        _a.var = "bar";
    })();
    return Example;
}());
console.log(Example.var);
```


このトランスパイル後のファイルを `node out/syntax.js` で実行すると、エラーなく動作します。

このように、`target` に所望の ECMAScript のバージョンを指定することで、そのバージョンで動作するように変換されるわけです。

## lib と API の関係

`lib` は、TypeScript の **型チェック** 時に影響します。その例として、`Array.at` を取り上げます。

このメソッドは ECMAScript 2022 で追加されました。したがって、`lib` に `"es2021"` を指定した場合、TypeScript の型チェック時に `Array.at` が存在しないため、エラーが発生します。

### 型チェックエラーの確認

まず、`api.ts` を作成し、以下のコードを記述します。

```ts:api.ts
const arr = [1, 2, 3];
console.log(arr.at(-1));
```

次に、`tsconfig.json` の `lib` を `"es2021"` に設定し、`npx tsc` を実行してみましょう。
("DOM" も指定しないと、`console.log()` 等の標準APIで型チェックエラーが発生するので指定しておきましょう)

```json:tsconfig.json
{
    "compilerOptions": {
        ...
        "lib": ["es2021", "DOM"]
        ...
    }
}
```

次のような型チェックエラーが発生するはずです。

```sh
error TS2550: Property 'at' does not exist on type 'number[]'.
```

そこで、`lib` を `"es2022"` に変更して再度 `npx tsc` を実行してみましょう。エラーは解消され、トランスパイルが成功するはずです。

**しかし、重要なのはここからです。**


出力された `out/api.js` を確認すると、`Array.at` は変換されずにそのまま残っています。

```javascript:out/api.js
"use strict";
var array = [0, 1, 2];
console.log(array.at(2));
```


繰り返しますが、Node.js 16 では "es2021"までしかサポートしていないため、`Array.at` はサポートされていません。したがって、`node out/api.js` を実行するとランタイムエラーが発生します。

指定バージョンで動くように**構文変換**してくれる `target` のバージョン指定とは違い、`lib` は何の変換にも関与しません。あくまでも型チェックで参照する型ファイルのバージョンを指定できるだけです。


### `lib` と `target` を別々に設定できる理由

Node.js 16 で動作させるために、`target` には `es2021` を指定しました。一方で、`Array.at` を使うため（型チェックを通すため）に、`lib` には `es2022` を指定しました。

「どうせランタイムエラーになるのなら、`lib` に `target` 以上のバージョンを指定する意味はあるのか？」という疑問が生じるかもしれません。しかし、`lib` と `target` に別のバージョンを設定できるようになっているのにはちゃんと理由があります。

例えば、実行環境が対応していないと分かりつつも、スマートな書き味を求めて新しい API でコードを書きたい！というケースはあるでしょう。先ほど例に上げた `Array.at(-1)` を使えば配列の末尾の要素を簡単に取得できます。`lib` に `es2022` を指定しておけば、Typescriptでコードを書いている分には問題なくそのAPIが使えるわけです。

とはいえ何度も言うように、実行環境がそのAPIをサポートしていなければランタイムエラーになってしまいます。
したがって、このようなケースにおいては、何らかの方法でこのランタイムエラーを解決する必要があります。

### Polyfillでランタイムエラーを回避する

本稿の例では `Array.at` のランタイムエラーを取り上げてきました。これを解決するには、何らかの方法で `Array.at` の実装を差し込んであげる必要があります。このようにギャップを埋めるために差し込むコードを **Polyfill** と言います。

自分で実装しても良いのですが、[`core-js`](https://www.npmjs.com/package/core-js) を使用すると簡単です。

まずはパッケージをインストールします。

```sh
npm install core-js
```

続いて `api.ts` に `import 'core-js/actual/array/at';` を追加します。

```ts:api.tsx
import 'core-js/actual/array/at';
const arr = [1, 2, 3];
console.log(arr.at(-1));
```

そして `npx tsc` を実行し、トランスパイル後のコードを確認してみましょう。

```js:out/api.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
require("core-js/actual/array/at");
var array = [0, 1, 2];
console.log(array.at(2));

```

インストールしたパッケージが import(本例ではrequireですが）されていることが分かります。実際のコードは[こんな感じ](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.array.at.js)。

Polyfill を差し込んだおｋ`node out/api.js` を実行してみると正常に動作するはずです。

## まとめ

TypeScript の `target` はどのバージョンの**構文（シンタックス）** に変換するかを決定し、`lib` は **型チェックの基準となる API** を決定します。`target` を古く、`lib` を新しく設定することで開発時の利便性が向上しますが、実行時には Polyfill が必要になります。

## 参考

- [tsconfig.jsonを設定する | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/tsconfig/tsconfig.json-settings)
- [TypeScript で polyfill やトランスパイルをどう扱えばいいか - Panda Noir](https://www.pandanoir.info/entry/2022/12/20/121301)