TypeScript 4.5 変更点 まとめ
2021-10-02(JST)、TypeScript 4.5 Betaが出た。
- Announcing TypeScript 4.5 Beta
-
TypeScript 4.5 Iteration Plan
- RCは11/02, Releaseは11/16の予定
TypeScriptは9歳らしい。
実験として、package.jsonとかのプロパティを表すのにjqの記法を使ってみています。飽きたら戻す
ECMAScript Module Support in Node.js
ES Modules 対応は TS 4.5 には搭載されないことになったので隠しています
Node.jsのECMAScript Modules (ES Modules, ESM) 対応が進んできて安定し始めた。ES Modules使用時の--experimental-modules
はv13.2.0, v12.17.0以降不要になり、警告もv14.0.0, v13.14.0, v12.20.0以降出なくなった。
ということで、tsconfig.jsonの.compilerOptions.module
に新しい値、node12
とnodenext
を追加する。
{
"compilerOptions": {
"module": "nodenext",
}
}
この新しいモードを有効化すると以下の機能が使えるようになる。これを有効化するのを忘れていて全然はたらかないじゃないかとうんうん唸っていた。
package.json
の type
に対応
ES Modules 対応は TS 4.5 には搭載されないことになったので隠しています
package.json
の.type
を使うと.js
拡張子のファイル[1]をCommonJS、ES Modulesのどちらで扱うか設定できる。
{
"type": "module"
}
node12
とnodenext
では、TypeScriptのtsc
も.js
,.jsx
,.ts
,.tsx
ファイルに対して.type
の指定を読み取るようになる。
ソースファイルがES Modulesとしてコンパイルされる場合、いくつかのルールが変更される。
たとえばこのような2つソースファイルがある。
export const bar = 33;
import { bar } from "./bar";
export const a = 2 + bar;
これらはCommonJSとしてトランスパイルすると
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = void 0;
exports.bar = 33;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.a = void 0;
const bar_1 = require("./bar");
exports.a = 2 + bar_1.bar;
のようになる。一方でES Modulesとしてはトランスパイルすることができない。
index.ts:1:21 - error TS2307: Cannot find module './bar' or its corresponding type declarations.
1 import { bar } from "./bar";
~~~~~~~
Found 1 error.
このエラーはES Modulesで追加される「相対インポート時に拡張子が必須」というルールによるもの。拡張子 .js
を補って
- import { bar } from "./bar";
+ import { bar } from "./bar.js";
export const a = 2 + bar;
とするとトランスパイルが可能になり、
import { bar } from "./bar.js";
export const a = 2 + bar;
export const bar = 33;
というような、ES ModulesなJSコードが出力される。ほとんどそのまま型情報を落としただけ。
ES Modulesを書いている場合、インポート候補には.js
だけが出てくるらしい。
-
.cjs
でCommonJS、.mjs
でES Modulesを明示的に指定できる。 ↩︎
.mts
.cts
新しいファイル拡張子 ES Modules 対応は TS 4.5 には搭載されないことになったので隠しています
つまるところ、package.json
の.type
を設定すると.js
と.ts
をCommonJSとES Modulesのどちらとして扱えばいいかがまとめて設定できるようになったわけだが、時には.type
が"commonjs"
だけどこのファイルはES Modulesとして扱いたい、という場面もある。webpackとかに突っ込むコードはCommonJSだけど小さいスクリプトには(top-level awaitとか使いたいし)ES Modulesがいい、みたいな。
Node.jsではすでに、ES Modulesに.mjs
、CommonJSに.cjs
という拡張子を使うことで.type
を無視したモジュールシステム指定ができるようになっているので、TypeScriptでも.mts
、.cts
という拡張子でモジュールシステムを指定できるようになる。トランスパイル時には.mts
は.mjs
へ、.cts
は.cjs
へと変換される。
さらに、.d.ts
についても同様の拡張子サポートが追加される。.mts
から生成された宣言ファイルは.d.mts
、.cts
から生成された宣言ファイルは.d.cts
という拡張子になる。
.tsx
について .mtsx
とか .ctsx
みたいなのが追加されてたりはしないっぽい。
package.json
の exports
フィールド
ES Modules 対応は TS 4.5 には搭載されないことになったので隠しています
package.json
の.exports
フィールドというものがある。これは.main
の強化版で、細かく指定できる。node12
nodenext
ではこれに対応する。
// コピペする場合コメントは除去すること
{
"name": "my-pkg",
"type": "module",
"exports": {
".": {
// import "my-pkg";
"import": "./esm/index.js",
// require("my-pkg");
"require": "./cjs/index.cjs"
}
},
// 旧バージョンのNode.js向けのフォールバックとしてmainを指定したほうがいいっぽい?
"main": "./cjs/index.cjs"
}
これまでTypeScriptは.main
フィールドを読んでいた。たとえば"main": "./lib/index.js"
とあればTypeScriptは型定義を"./lib/index.d.ts"
にあると推測するが、パッケージの作者は"types": "./types/index.d.ts"
のようにして別の場所を教えることもできる。
.exports
を利用する場合、ES Modulesなソースファイルからimport "pkg"
すると.exports["."].import
が、CommonJSなソースファイルからimport "pkg"
すると.exports["."].require
が参照される。やはり、対応する型定義ファイルは.exports["."].types
から明示的に指定することもできる。
// コピペする場合コメントは除去すること
{
"name": "my-pkg",
"type": "module",
"exports": {
".": {
// import "my-pkg";
"import": "./esm/index.js",
// require("my-pkg");
"require": "./cjs/index.cjs",
// 別の場所に型定義を置く
"types": "./types/index.d.ts"
}
},
"main": "./cjs/index.cjs",
// 古いTypeScript向けのフォールバックとしてtypesを指定
"types": "./types/index.d.ts"
}
node_modules
から lib
を追加できるようになった
typescriptにはECMAScriptやブラウザのDOM APIの型定義が同梱されている。これらの定義ファイルはtsconfig.json
の.compilerOptions.target
からいい感じに読み込まれるが、 .compilerOptions.lib
を使って手動で指定することもできる。
現状の仕組みには問題点が2つある。
- 同梱されている型定義がTypeScriptのアップグレードに引っ張られて変更されてしまう。DOM APIは頻繁に変更されている。
- 依存パッケージのために手動で
lib
を書き換える必要がある。例えばDOMを使うライブラリに依存しているなら"lib": ["DOM", "DOM.Iterable"]
のような指定が要る
これを解決するためにTS 4.5では "lib": ["<name>"]
の読み込み時に @typescript/lib-<name>
というパッケージを参照するようになる。"lib": ["dom"]
とあればまず@typescript/lib-dom
の読み込みを試みる。
npmjs.comの @typescript
スコープは TypeScriptチームが確保しているので我々一般開発者がオレオレlibを @typescript
にぶち込むようなことは当然できないのだが、package.json
を編集して @typescript/lib-dom
に別のパッケージを割り当てると、参照するdom
をまったく別のものに差し替えることができる。
{
"dependencies": {
"@typescript/lib-dom": "npm:@types/web"
}
}
ここで出てきた @types/web
というパッケージは DOMなどWebブラウザで使えるAPIをまとめたものであり、これのバージョンを指定しておけばTypeScriptのアップグレード時にdom
に破壊的変更が入ってぶっ壊れるようなことが避けられる。
この新機能を利用したlib
差し替えパッケージが存在するので、機能自体も含めた解説記事を貼る。
Awaited
と Promise.all
などの型定義の改善
ユーティリティ型
Awaited<T>
というユーティリティ型が追加される。この型はその名前から想像できるように、T
型の値t
について await t
の型である。
// 全部number
type A1 = Awaited<number>; // await 1に相当
type A2 = Awaited<Promise<number>>; // await Promise.resolve(1)に相当
type A3 = Awaited<Promise<Promise<number>>>; // await Promise.resolve(Promise.resolve(1))に相当(Promiseのネストはawait時にflattenされる)
type A4 = Awaited<PromiseLike<number>>; // いわゆる“thenable”オブジェクトに対するawait
type A5 = Awaited<number | Promise<number> | PromiseLike<number>>;
これを利用してPromise.all
などの型推論が賢くなった。
type MaybePromise<T> = T | PromiseLike<T> | Promise<T>;
async function doSomething(
p: readonly MaybePromise<number>[]
) {
// TS<4.5: const result: (number | Promise<number>)[]
// TS=4.5: const result: number[]
const result = await Promise.all(p);
// TS<4.5: Operator '+' cannot be applied to types 'number | Promise<number>' and 'number | Promise<number>'. ts(2365)
// Did you forget to use 'await'?
// TS=4.5: エラーなし
return result.reduce((acc, val) => acc + val);
}
microsoft/TypeScript/v4.5-betaのlib/lib.es5.d.ts より Awaited<T>
の定義を以下に引用する。Conditional Typesを駆使してawait
の挙動を模倣していることがわかる。
/**
* Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to `never`. This emulates the behavior of `await`.
*/
type Awaited<T> =
T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode
T extends object & { then(onfulfilled: infer F): any } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
F extends ((value: infer V) => any) ? // if the argument to `then` is callable, extracts the argument
Awaited<V> : // recursively unwrap the value
never : // the argument to `then` was not callable
T; // non-object or non-thenable
続いてlib/lib.es2015.promise.d.tsの Promise.all
をv4.4.3とv4.5-betaで比較してみよう。
おもむろにdetailsを用意…
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3, T4, T5, T6, T7, T8, T9>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9]>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3, T4, T5, T6, T7, T8>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8]>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3, T4, T5, T6, T7>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>]): Promise<[T1, T2, T3, T4, T5, T6, T7]>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3, T4, T5, T6>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>]): Promise<[T1, T2, T3, T4, T5, T6]>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3, T4, T5>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>]): Promise<[T1, T2, T3, T4, T5]>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3, T4>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>]): Promise<[T1, T2, T3, T4]>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2, T3>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T1, T2>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1, T2]>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T>(values: readonly (T | PromiseLike<T>)[]): Promise<T[]>;
型引数T
は解決される値の型を表しており、タプルに対応するために引数の数によってオーバーロードされていてとても長いし、要素数が10を超えたものには型付けできていない。
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;
型引数 T
は引数となる配列の型に変更されており、Mapped typesを使って T
の要素それぞれにAwaited
を適用している。
Template Literal Types as Discriminants
TypeScriptでは文字列リテラル型を用いて直和型(tagged union)を表現する手法が広く行われている。
export interface Success {
type: `Success`;
body: string;
}
export interface Error {
type: `Error`;
message: string;
}
export function handler(r: Success | Error) {
if (r.type === "Success") {
// rはSuccess型
let token = r.body;
}
}
ここで文字列リテラル型の代わりにテンプレートリテラル型を使うことができるようになった。
export interface Success {
type: `${string}Success`;
body: string;
}
export interface Error {
type: `${string}Error`;
message: string;
}
export function handler(r: Success | Error) {
// Successで終わる文字列型
if (r.type === "HttpSuccess") {
// rはSuccess型
let token = r.body;
}
}
ユースケースがパッと思いつかないです。
--module es2022
の追加
コンパイラオプション module
に es2022
が追加されました。この設定では ES2022 に入る top-level await、すなわち async
関数の外側での await
ができるようになります。ほかにtop-level awaitが使えるmodule
の値は以前から使えた esnext
、同じく4.5で追加された nodenext
があり、合計して3つ、es2022
は安定して使える初めての設定[1]になります。
-
esnext
,nodenext
は将来のTypeScriptのバージョンアップ時、ECMAScriptやNode.jsの機能追加に合わせて機能が更新されていくため ↩︎
Conditional Types での末尾再帰最適化
Conditional Typesで型レベル末尾再帰最適化を行うらしい。
以下のような型が最適化の対象になる。Conditional typesの分岐のうち片方がTrimLeft<Rest>
というような再帰呼び出しになっている場合最適化が行われる。
type TrimLeft<T extends string> = T extends ` ${infer Rest}`
? TrimLeft<Rest>
: T;
type Test = TrimLeft<" hi!">;
type Test2 =
TrimLeft<" aieee!">;
type Last<T, L = never> = T extends [infer F, ...infer Rest]
? Last<Rest, F> // ここでLastを呼び出す
: L;
type a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
type l = Last<[...a, ...a, ...a, ...a, ...a, 3]>;
再帰呼び出ししたあとunionなどが使われていると最適化されない。
type Chars<S> = S extends `${infer Char}${infer Rest}` ? Char | Chars<Rest> : S;
// Type instantiation is excessively deep and possibly infinite. ts(2589)
type c =
Chars<"うわなにをするくぁwせdrftgyふじこlp;「’」qawsedrftgyhujikolp;[']!@#$%^&*()_+|~">;
このような型は、型引数にアキュムレータ(結果を保持するための型引数)を導入してConditional typesの分岐(branch)を再帰呼び出しだけにすれば最適化されるようになる。
export type Chars2<S> = Chars2Inner<S, never>;
type Chars2Inner<S, Acc> = S extends `${infer Char}${infer Rest}`
? Chars2Inner<Rest, Acc | Char>
: Acc | S;
// OK
type c2 =
Chars2<"うわなにをするくぁwせdrftgyふじこlp;「’」qawsedrftgyhujikolp;[']!@#$%^&*()_+|~">;
末尾再帰の概念について参考になったブログを貼っておきます。
Disabling Value Elision
新しいコンパイラオプション --preserveValueImports
を導入する。--preserveValueImports
が有効な場合、未使用の(正確にはtscの解析で未使用と判断された)値インポートが削除されなくなる。tscの解析では未使用とみなされているが実際にはtscが解析できない場所で使用されている場合に有効。
import { readFile } from "node:fs/promises";
eval(`readFile("./package.json", "utf-8").then(console.log);`);
evalはともかく、.vue
のscript setup
を使っている場合や.svelte
といった場面で有効らしい。
<script>
import { someFunc } from "./some-module.js";
</script>
<button on:click={someFunc}>Click me!</button>
<script setup lang="ts">
import { someFunc } from "./some-module.js";
</script>
<button @click="someFunc">Click me!</button>
--preserveValueImports
と --isolatedModules
の併用時には制限が加わる。
--isolatedModules
は、esbuildやts-loaderといったトランスパイラが変換できるソースになっていない場合にエラーを出すオプションである。esbuildなどは1ファイルで完結した変換を行うため、変換に際して別ファイルにある型定義を参照することができない。たとえば import { PathLike } from "node:fs";
という文を見たとき、tscはnode:fs
の型定義を参照することでPathLike
が型であると判別できるが、esbuildにはそれがわからないのだ。
以下のコードは構文を解析するだけで「これは型でこれは値だ」と判別できるだろうか?
import { PathLike } from "node:fs";
export { PathLike };
// 略記法
export { PathLike } from "node:fs"
"node:fs"
から PathLike
という識別子をインポートし、再びエクスポートしている。これは PathLike
が値でも型でも構文として正しいので、判別できない。
- 値だった場合、実行時に他からインポートされるはずなので、消してはいけない。
- 型だった場合、トランスパイル時に消す必要がある。さもなくば、実行時に「そのようなモジュールは存在しない」と言われてエラーになってしまう。
このように、型定義を見ないで PathLike
が型であるか否か判別することが求められ、詰み状態になってしまった。--isolatedModules
が有効な場合、tscはエラーを出すだろう。
解決法があるのだがいったん置いておいて、--preserveValueImports
に戻る。以下のようなコードを考える。
import { readFile, PathLike } from "node:fs";
// ... ここでreadFileを使ってるけどコンパイラはそれを知らない
-
--preserveValueImports
ではない場合、readFile
もPathLike
も使われていないのでまとめて消してしまえる。- だが、
--preserveValueImports
がある以上、見た目上は使われていなさそうでも実際には使われているかもしれない。うかつに消すことはできない。
- だが、
- 実際のコードでの使われ方から探ることはできるだろうか?
- いや、
--preserveValueImports
が必要になるような場面では、そもそも実際にインポートしたものが使われているところを観測できないのだから、使われ方から探ることはできない。
- いや、
- そのまま残してしまうか?
- しかし、型のインポートはこのタイミングで消去しなければ実行時に「そのようなモジュールは存在しない」と言われてエラーになってしまう。
やはり、型定義を参照せずして PathLike
が型であることを知らなければならないことになってしまった。--isolatedModules
--preserveValueImports
がともに有効な場合、tscはエラーを出すだろう。
この問題は「型定義を参照しないで PathLike
が型であるか否かを知らなければならない」という問題である。これの解決法はずばり「型定義を参照しなくても構文を見ればそれが型であると判別できる」という構文を追加することで、それが TypeScript 3.8で追加された型だけインポート/エクスポート だ。import
export
の後に type
をつけることでこれは型ですよ~と教えることができ、esbuildなんかはちゃんと読み取ってさっぱり消してくれる。どうしてこんなに説明に分量を使っているのかわからないがこれで解決。
import type { PathLike } from "node:fs";
export { PathLike };
// 略記法
export type { PathLike } from "node:fs";
import { readFile } from "node:fs";
import type { PathLike } from "node:fs";
import
での type
修飾子 (?)
前述の型だけインポート/エクスポート、実は気になるところが1個残っている。普通のインポートでは
import { readFile, PathLike } from "node:fs";
のように1行でまとめて書けていたのだが、type
をつける場合 import
とは分ける必要があるので
import { readFile } from "node:fs";
import type { PathLike } from "node:fs";
と書かなければいけない。これは冗長なので、TS 4.5では識別子の前に type
をつけることでそれが型だと示せるようになった。
import { readFile, type PathLike } from "node:fs";
これは4.5で唯一っぽい独自構文の追加。
プライベートプロパティの存在チェック
ES2022に入る予定のプロポーザル。'foo' in obj
のプライベートプロパティ版で、#foo in obj
という形の式で obj
が存在するなら true
に評価される。
class Person {
#name;
#age;
constructor(name: string, age: number) {
this.#name = name;
this.#age = age;
}
equals(other: unknown) {
return (
other && // falsyを除去
typeof other === "object" && // プリミティブを除去
#name in other && // Personの#nameが存在するかチェック
this.#name === other.#name && // 比較1。ここでotherはPersonに推論される
this.#age === other.#age // 比較2。#ageの存在チェックをしていないことに注目
);
}
}
// true
console.log(new Person("Charlotte", 17).equals(new Person("Charlotte", 17)));
この構文の面白いところは、#name
プロパティを持ったオブジェクトが Person
のインスタンス以外に存在しないことから、#name in other
が true
と評価された時点で other
が Person
であると確定できるところにある。
「ちょっと待った、#name in other
で other
が Person
ではないが #name
を持っているオブジェクトの場合はどうするのか?」という疑問が当然浮かんでくる。しかし、2つのクラスが互いに異なる限り、双方に #name
というプロパティがあったとしてもそれらは異なるものとして扱われる。
class Person {
#name;
#age;
constructor(name: string, age: number) {
this.#name = name;
this.#age = age;
}
equals(other: unknown) {
return (
other &&
typeof other === "object" &&
#name in other && // **Personの** #nameが存在するかチェック。Person2の#nameではだめ
this.#name === other.#name && // 比較1。ここでotherはPersonに推論される
this.#age === other.#age // 比較2。やはり#ageの存在チェックをしていない
);
}
}
class Person2 {
#name;
#age;
constructor(name: string, age: number) {
this.#name = name;
this.#age = age;
}
}
// false
console.log(new Person("Ange", 17).equals(new Person2("Ange", 17)));
これが許されたらプライベートプロパティ読み放題になってしまうので妥当そう。
Import Assertions
TC39 Stage 3 Proposalの対応。
Import assertionsはインポートしたものが本当に所望のフォーマットになっているか ランタイムで 検証させる機能。検証をやるのはブラウザなどの実行環境であってTypeScriptではないことに注意。
import p from "./package.json" assert { type: "json" };
// dynamic import では第2引数
const p = import("./package.json", { assert: { type: "json" } });
// このようなコードもtscを通るがブラウザでは動かない
import p from "./package.json" assert { type: "🤪" };
import()
の第2引数はImportCallOptions
という型としてlib.es5.d.tsに定義されている。
tsc --init
で生成されるtsconfig.jsonの.compilerOptions.target
がes2016
に変更された。