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

はじめに

この記事は、TypeScript 4.3で導入されたプライベートクラスフィールドの利用法を共有するためのものです。

先に結論だけ

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

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

プライベートクラスフィールドとはクラス外部から参照不能な変数、関数です。変数や関数の名前を#で始めると、JavaScriptエンジンはそのフィールドをプライベートだと認識します。この仕様はECMAScriptで提案されています。ステージは3です。

参考 : MDN Web Docs プライベートクラスフィールド

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

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

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

参考記事 : TypeScript 3.8の新機能「ハードプライベート」と従来の「ソフトプライベート」を比べてみた

TypeScriptのprivate修飾子との違い

いままでもTypeScriptにはprivate修飾子がありました。今回導入されたプライベートクラスフィールドと、private修飾子の違いをTypeScript Playgroundで確認します。

テストコードは以下の通りです。

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とします。

コンパイルエラー

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'の外からはアクセスできません。

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

型定義ファイル

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

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

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

JavaScriptファイル

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

"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クラスは以下のようになります。

"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に変換されているのが分かります。

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

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