はじめに
TypeScript の tsconfig.json
では、target
と lib
に "es2015"
, "es2020"
, "esnext"
などの ECMAScript バージョンを指定できます。しかし、これらの違いを明確に理解するのは意外と難しく、私自身も混乱したことがありました。個人的には特に lib
の存在意義がなかなか理解できませんでした。
この記事では、target
と lib
の違いをより分かりやすく説明するために、実際に手を動かしながら検証していきます。
検証環境の用意
特に特殊なことはしてません。自分で用意できる方、あるいは検証する必要がない方は読み飛ばしてください。
今回の検証には Node.js 16 を使用します。(本当はもっと古いバージョンを使いたかったのですが、セットアップが謎に大変で断念)
Node.js 16 は ECMAScript 2021 までをサポートしています。
まずは、お手元の環境に Node.js 16 をインストールしてほしいのですが、Docker を使用すれば手軽に準備できます。
検証用のディレクトリを作成し、以下の Dockerfile
と devcontainer.json
を用意して、VS Code から devcontainer を開いてください。
FROM node:16.0.0
CMD ["sleep", "infinity"]
{
"name": "Development Container",
"build": {
"dockerfile": "../Dockerfile",
"context": ".."
}
}
Node.js 16 が動作する環境を用意できたら、まずは package.json
を生成します。
npm init -y
次に、TypeScript をインストールします。
npm install -D typescript
最後に、本稿の主役である tsconfig.json
を生成します。
npx tsc --init
なお、トランスパイル後のファイルの出力先として、tsconfig.json
の outDir
は "./out"
に設定しておきます。
{
"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
で実行してみましょう。ランタイムエラーが発生するはずです。
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"
に設定してトランスパイルしましょう。
npx tsc
出力された out/syntax.js
を確認すると、static
ブロックが別の構文に変換されていることがわかります。
"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
で実行すると、エラーなく動作します。
このように、lib
に所望の ECMAScript のバージョンを指定することで、そのバージョンで動作するように変換されるわけです。
lib と API の関係
lib
は、TypeScript の 型チェック 時に影響します。その例として、Array.at
を取り上げます。
このメソッドは ECMAScript 2022 で追加されました。したがって、lib
に "es2021"
を指定した場合、TypeScript の型チェック時に Array.at
が存在しないため、エラーが発生します。
型チェックエラーの確認
まず、api.ts
を作成し、以下のコードを記述します。
const arr = [1, 2, 3];
console.log(arr.at(-1));
次に、tsconfig.json
の lib
を "es2021"
に設定し、npx tsc
を実行してみましょう。
("DOM" も指定しないと、console.log()
等の標準APIで型チェックエラーが発生するので指定しておきましょう)
{
"compilerOptions": {
...
"lib": ["es2021", "DOM"]
...
}
}
次のような型チェックエラーが発生するはずです。
error TS2550: Property 'at' does not exist on type 'number[]'.
そこで、lib
を "es2022"
に変更して再度 npx tsc
を実行してみましょう。エラーは解消され、トランスパイルが成功するはずです。
しかし、重要なのはここからです。
出力された out/api.js
を確認すると、Array.at
は変換されずにそのまま残っています。
"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
を使用すると簡単です。
まずはパッケージをインストールします。
npm install core-js
続いて api.ts
に import 'core-js/actual/array/at';
を追加します。
import 'core-js/actual/array/at';
const arr = [1, 2, 3];
console.log(arr.at(-1));
そして npx tsc
を実行し、トランスパイル後のコードを確認してみましょう。
"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ですが)されていることが分かります。実際のコードはこんな感じ。
Polyfill を差し込んだおknode out/api.js
を実行してみると正常に動作するはずです。
まとめ
TypeScript の target
はどのバージョンの構文(シンタックス) に変換するかを決定し、lib
は 型チェックの基準となる API を決定します。target
を古く、lib
を新しく設定することで開発時の利便性が向上しますが、実行時には Polyfill が必要になります。