TypeScript 4.3でプライベートクラスフィールド(#)が導入されました

## はじめに

この記事は、TypeScript 4.3で導入された[プライベートクラスフィールド](https://devblogs.microsoft.com/typescript/announcing-typescript-4-3/#private-elements)の利用法を共有するためのものです。

## 先に結論だけ

-   プライベートクラスフィールドは、変換後のJavaScriptファイルでも維持されます。
-   プライベートメンバーにアクセスしようとすると、ランタイムエラーが発生します。
-   コンパイルエラーだけのprivate修飾子よりも、より厳密にアクセスを制限できます。

## プライベートクラスフィールドとは

プライベートクラスフィールドとはクラス外部から参照不能な変数、関数です。変数や関数の名前を`#`で始めると、JavaScriptエンジンはそのフィールドをプライベートだと認識します。この仕様はECMAScriptで[提案](https://github.com/tc39/proposal-class-fields)されています。ステージは3です。

参考 : [MDN Web Docs プライベートクラスフィールド](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes/Private_class_fields)

変数のプライベート化はTypeScript 3.8の時点で対応していましたが、4.3では

- 関数
- static変数
- static関数
- アクセサーメソッド

のプライベート化にも対応しています。

参考記事 : [TypeScript 3.8の新機能「ハードプライベート」と従来の「ソフトプライベート」を比べてみた](https://qiita.com/suin/items/fafc81be5a7222648863)

## TypeScriptのprivate修飾子との違い

いままでもTypeScriptにはprivate修飾子がありました。今回導入されたプライベートクラスフィールドと、private修飾子の違いを[TypeScript Playground](https://www.typescriptlang.org/play/)で確認します。

[テストコード](https://www.typescriptlang.org/play?target=99&strict=true&noImplicitOverride=true&ts=4.3.0-beta#code/MYGwhgzhAEBiD28DeAoa7oAcBOBLAbmAC4Cm0ARmNtALzQBEl29A3GhgMSUBetDXYbqxTt02EmAAmABTyFSsXCRCSIACgCUqDDujB4AOwjwQJAHQh4AczXQiAC1wQzTaBra6M+oyfOWbdo7OArzuotAAvihRKN4QRNAAZoh8BiQA7nCImizQKMnwZuJSsgTEJIrKqjnQAPS10IDKDI2A9gyAxtaAHHqA1gyAq0qAOvKAmgyA0QyxhsamFta2BS5Ubrn10ACi2Njw1ABc0LLwmCTYRACe0ADkTCfQTlhy5dBgBpLQhiDHYMDAJFC45KbQ6bgOXAGPTgKCnBDwE5mUY+Cb+aaIMwheZ1BorNabbZrPYHY4nEIXK4GeAJN4fL4-MjwACuRAguEkZFAkBgJwhF3IJGAYGpEDIAOg9kgd2uZVIl0ZBiIuESSmwZiAA)は以下の通りです。

```typescript
class Foo{
    private bar: string = "bar";
    #baz:string = "#baz";

    readPrivateFields(){
        console.log( this.bar );
        console.log( this.#baz );
    }
}

const foo = new Foo();
foo.readPrivateFields(); // ここは正常に動作する
console.log( foo.bar ); // Error : Property 'bar' is private and only accessible within class 'Foo'.
console.log( foo.#baz ); // Error : Property '#baz' is not accessible outside class 'Foo' because it has a private identifier.
```

tsconfigのターゲットはESNextとします。

### コンパイルエラー

```typescript
console.log( foo.bar );
// Error : Property 'bar' is private and only accessible within class 'Foo'.
// プロパティ「bar」はプライベートなもので、クラス「Foo」内でのみアクセス可能です。
console.log( foo.#baz );
// Error : Property '#baz' is not accessible outside class 'Foo' because it has a private identifier.
// プロパティ'#baz'は、プライベート識別子を持っているため、クラス'Foo'の外からはアクセスできません。
```

このコードをそのまま実行すると、コンパイルエラーが発生します。エラーメッセージが変更されているのが分かります。

### 型定義ファイル

出力される型定義ファイルは以下のようになります。

```typescript
declare class Foo {
    #private;
    private bar;
    readPrivateFields(): void;
}
declare const foo: Foo;
```

注目すべきポイントは#bazの定義です。#bazは型定義ファイルには出力されません。

### JavaScriptファイル

出力されるJavaScriptファイルのうち、Fooクラス部分は以下のようになります。

```js
"use strict";
class Foo {
    constructor() {
        this.bar = "bar";
        this.#baz = "#baz";
    }
    #baz;
    readPrivateFields() {
        console.log(this.bar);
        console.log(this.#baz);
    }
}
```

`bar`変数のprivate修飾子が消えています。`#baz`変数の#修飾は維持されています。

型定義ファイルを捨て、出力されたJavaScriptファイルのみをモジュールとしてimportした場合、bar変数にはアクセスできます。#baz変数にはアクセスできません。

ターゲットをES2015に変更すると、Fooクラスは以下のようになります。

```js
"use strict";
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _Foo_baz;
class Foo {
    constructor() {
        this.bar = "bar";
        _Foo_baz.set(this, "#baz");
    }
    readPrivateFields() {
        console.log(this.bar);
        console.log(__classPrivateFieldGet(this, _Foo_baz, "f"));
    }
}
_Foo_baz = new WeakMap();
```

`#baz`変数が[WeakMap](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)に変換されているのが分かります。

プライベートクラスフィールドが導入されても、private修飾子をすぐに置き換える必要はありません。しかし、将来的なメンテナンス性を考えると、少しずつ置き換えていくのが良さそうです。

以上、ありがとうございました。