Closed16

TypeScript 4.5 変更点 まとめ

おーみーおーみー

ECMAScript Module Support in Node.js

ES Modules 対応は TS 4.5 には搭載されないことになったので隠しています

https://github.com/microsoft/TypeScript/pull/44501

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に新しい値、node12nodenextを追加する。

tsconfig.json
{
    "compilerOptions": {
        "module": "nodenext",
    }
}

この新しいモードを有効化すると以下の機能が使えるようになる。これを有効化するのを忘れていて全然はたらかないじゃないかとうんうん唸っていた。

おーみーおーみー

package.jsontype に対応

ES Modules 対応は TS 4.5 には搭載されないことになったので隠しています

package.json.typeを使うと.js拡張子のファイル[1]をCommonJS、ES Modulesのどちらで扱うか設定できる。

package.json
{
  "type": "module"
}

node12nodenextでは、TypeScriptのtsc.js,.jsx,.ts,.tsxファイルに対して.typeの指定を読み取るようになる。

ソースファイルがES Modulesとしてコンパイルされる場合、いくつかのルールが変更される。

たとえばこのような2つソースファイルがある。

bar.ts
export const bar = 33;
index.ts
import { bar } from "./bar";
export const a = 2 + bar;

これらはCommonJSとしてトランスパイルすると

bar.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.bar = void 0;
exports.bar = 33;
index.js
"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 を補って

index.ts
- import { bar } from "./bar";
+ import { bar } from "./bar.js";
export const a = 2 + bar;

とするとトランスパイルが可能になり、

index.js
import { bar } from "./bar.js";
export const a = 2 + bar;
bar.js
export const bar = 33;

というような、ES ModulesなJSコードが出力される。ほとんどそのまま型情報を落としただけ。

ES Modulesを書いている場合、インポート候補には.jsだけが出てくるらしい。

脚注
  1. .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.jsonexports フィールド

ES Modules 対応は TS 4.5 には搭載されないことになったので隠しています

package.json.exportsフィールドというものがある。これは.mainの強化版で、細かく指定できる。node12 nodenextではこれに対応する。

package.json
// コピペする場合コメントは除去すること
{
  "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から明示的に指定することもできる。

package.json
// コピペする場合コメントは除去すること
{
  "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 を追加できるようになった

https://github.com/microsoft/TypeScript/pull/45771

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をまったく別のものに差し替えることができる。

package.json
{
 "dependencies": {
    "@typescript/lib-dom": "npm:@types/web"
  }
}

ここで出てきた @types/web というパッケージは DOMなどWebブラウザで使えるAPIをまとめたものであり、これのバージョンを指定しておけばTypeScriptのアップグレード時にdomに破壊的変更が入ってぶっ壊れるようなことが避けられる。

この新機能を利用したlib差し替えパッケージが存在するので、機能自体も含めた解説記事を貼る。
https://zenn.dev/uhyo/articles/better-typescript-lib-v2

おーみーおーみー

ユーティリティ型 AwaitedPromise.all などの型定義の改善

https://github.com/microsoft/TypeScript/pull/45350

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で比較してみよう。

まずはv4.4.3でのPromise.allの定義

おもむろに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を超えたものには型付けできていない。

つぎにv4.4-betaでのPromise.allの定義

    /**
     * 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 の追加

https://github.com/microsoft/TypeScript/pull/44656

コンパイラオプション modulees2022 が追加されました。この設定では ES2022 に入る top-level await、すなわち async 関数の外側での await ができるようになります。ほかにtop-level awaitが使えるmoduleの値は以前から使えた esnext、同じく4.5で追加された nodenext があり、合計して3つ、es2022は安定して使える初めての設定[1]になります。

脚注
  1. esnext, nodenext は将来のTypeScriptのバージョンアップ時、ECMAScriptやNode.jsの機能追加に合わせて機能が更新されていくため ↩︎

おーみーおーみー

Conditional Types での末尾再帰最適化

https://github.com/microsoft/TypeScript/pull/45711

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;[']!@#$%^&*()_+|~">;

末尾再帰の概念について参考になったブログを貼っておきます。

https://rn4ru.com/blog/posts/tail-recursion/

おーみーおーみー

Disabling Value Elision

新しいコンパイラオプション --preserveValueImports を導入する。--preserveValueImports が有効な場合、未使用の(正確にはtscの解析で未使用と判断された)値インポートが削除されなくなる。tscの解析では未使用とみなされているが実際にはtscが解析できない場所で使用されている場合に有効。

import { readFile } from "node:fs/promises";

eval(`readFile("./package.json", "utf-8").then(console.log);`);

evalはともかく、.vuescript 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 ではない場合、readFilePathLike も使われていないのでまとめて消してしまえる。
    • だが、--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で唯一っぽい独自構文の追加。

おーみーおーみー

プライベートプロパティの存在チェック

https://github.com/tc39/proposal-private-fields-in-in

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 othertrue と評価された時点で otherPerson であると確定できるところにある。

「ちょっと待った、#name in otherotherPerson ではないが #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の対応。

https://github.com/tc39/proposal-import-assertions

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に定義されている

このスクラップは2022/01/22にクローズされました