モダンなWeb開発において、コードの堅牢性と保守性を高めることはエンジニアにとって最も重要な課題の一つです。

TypeScriptを用いた開発では、クラスの内部状態を適切に管理するために「アクセサー」と呼ばれるgetterとsetterの活用が欠かせません。

単に変数に値を代入するだけでなく、アクセサーを通じてカプセル化を実現することで、予期しないバグを未然に防ぎ、コードの可読性を劇的に向上させることが可能になります。

本記事では、2026年現在の開発シーンで求められるgetterとsetterの正しい実装方法について、実用的なコード例を交えながら詳しく解説します。

カプセル化とアクセサーの重要性

オブジェクト指向プログラミングにおけるカプセル化とは、データとそのデータを操作する手続きを一つにまとめ、外部から直接参照できないように隠蔽することを指します。

TypeScriptでは、private修飾子や#(プライベートフィールド)を使用することで、クラス外部からの直接的なアクセスを制限できます。

しかし、単に隠蔽するだけでは、外部から値を読み取ったり、特定のルールに従って値を更新したりすることができなくなります。

ここで登場するのがアクセサー(getter/setter)であり、内部データへの安全な入り口としての役割を果たします。

アクセサーを利用することで、開発者はプロパティの読み書き時に「バリデーション」や「値の変換」といったロジックを挟み込むことができるようになります。

TypeScriptにおけるgetterとsetterの基本構文

まずは、TypeScriptでgetterとsetterを定義するための標準的な構文を確認しましょう。

getterは引数を持たず、値を返すメソッドのように定義し、setterは新しい値を引数として受け取るように定義します。

TypeScript
// 基本的なアクセサーの実装例
class User {
    // 内部的なプロパティ名は慣習的にアンダースコアを付ける
    private _name: string;

    constructor(name: string) {
        this._name = name;
    }

    // nameプロパティを取得するためのgetter
    get name(): string {
        return this._name;
    }

    // nameプロパティを更新するためのsetter
    set name(value: string) {
        if (value.length === 0) {
            throw new Error("名前は空にできません。");
        }
        this._name = value;
    }
}

const user = new User("田中太郎");
console.log(user.name); // getterが呼ばれる

user.name = "佐藤次郎"; // setterが呼ばれる
console.log(user.name);
実行結果
田中太郎
佐藤次郎

この例では、外部からはuser.nameという公開プロパティを扱っているように見えますが、実際にはクラス内のメソッドを経由しています。

このように、利用側には複雑なロジックを意識させず、代入式という簡潔な記述でデータ操作を行えるのがアクセサーの大きなメリットです。

アクセサー定義時のコンパイル設定

TypeScriptでアクセサーを使用する場合、コンパイルターゲットが ES5 以降である必要があります。

現在の主要な開発環境では ESNextES2022 以上が標準となっているため問題になることは稀ですが、古いブラウザをサポートする場合は tsconfig.jsontarget 設定を確認しておきましょう。

プライベートフィールド(#)とアクセサーの組み合わせ

TypeScriptでは長年、内部変数に _property のようにアンダースコアを付ける命名規則が用いられてきました。

しかし、現在のJavaScript/TypeScriptでは、言語仕様としてサポートされた「プライベートフィールド(#)」を利用することが推奨される場面が増えています。

TypeScript
class Employee {
    // 真のプライベートフィールドを定義
    #salary: number;

    constructor(salary: number) {
        this.#salary = salary;
    }

    get salary(): number {
        // 給与情報を取得する際に単位を付けるなどの加工が可能
        return this.#salary;
    }

    set salary(value: number) {
        if (value < 0) {
            console.error("給与に負の値は設定できません。");
            return;
        }
        this.#salary = value;
    }
}

const emp = new Employee(300000);
emp.salary = -100; // setter内でのバリデーションが機能する
console.log(emp.salary);
実行結果
給与に負の値は設定できません。
300000

# を使用した変数は、クラスの外部から物理的にアクセスすることが不可能になるため、より厳格なカプセル化を実現したい場合に最適です。

アクセサーと組み合わせることで、堅牢なデータモデルを構築することができます。

アクセサーを活用する具体的なメリット

getterとsetterを単なる値の仲介役として使うだけでは、その真価は発揮されません。

実務で役立つ具体的な活用シーンを3つ紹介します。

1. 入力値のバリデーション(妥当性確認)

setterを使用する最大の利点は、不正なデータがオブジェクト内に混入するのを防ぐことです。

例えば、年齢を扱うプロパティであれば「0歳以上」であること、メールアドレスであれば「特定の形式に従っていること」をsetter内で保証できます。

2. 読み取り専用プロパティ(計算値)の作成

getterのみを定義し、setterを定義しないことで、外部からは書き換え不可能な「読み取り専用」の状態を作り出すことができます。

また、複数のプロパティから算出される値を、あたかも一つのプロパティのように見せる「計算済みプロパティ」としても活用されます。

TypeScript
class Product {
    constructor(public price: number, public taxRate: number) {}

    // 税込価格を計算して返すgetter
    get priceIncludingTax(): number {
        return Math.floor(this.price * (1 + this.taxRate));
    }
}

const item = new Product(1000, 0.1);
console.log(`税込価格: ${item.priceIncludingTax}円`);
実行結果
税込価格: 1100円

このように定義すれば、priceIncludingTax という変数を別途保持する必要がなくなり、常に最新の計算結果を取得できます。

3. デバッグとログ出力の容易化

大規模なアプリケーションにおいて、特定の変数が「いつ、どこで書き換えられたか」を追跡するのは困難です。

setterの中に console.trace() やログ出力ロジックを一時的に挿入することで、変数の変更タイミングを正確に把握できるようになります。

アクセサーと通常のメソッドの使い分け

「値を操作する」という点では、アクセサーも通常のメソッド(関数)も同じですが、どのように使い分けるべきでしょうか。

以下の表に、それぞれの特徴と推奨される用途をまとめました。

特徴アクセサー (getter/setter)メソッド (関数)
呼び出し方プロパティのようにアクセス (obj.val)関数として呼び出し (obj.getVal())
処理の内容軽量な処理、値の取得・設定が主重い計算、非同期処理、複数の引数が必要な処理
セマンティクス「状態」を表す「動作」を表す

基本的には、外部から見て「属性(プロパティ)」と感じられるものはアクセサーを使い、「アクション(動作)」と感じられるものはメソッドとして実装するのが適切です。

例えば、calculateTotal() はメソッドが適していますが、total はアクセサーが適しています。

実装時の注意点とアンチパターン

アクセサーは非常に強力ですが、誤った使い方をするとパフォーマンスの低下や可読性の阻害を招く恐れがあります。

無限ループに注意する

setterの中で自分自身に値を代入しようとすると、無限に再帰呼び出しが発生してしまいます。

必ず _name#name といった、バッキングフィールド(裏側の変数)に対して操作を行うようにしてください。

副作用を持たせすぎない

getterの中でAPIを叩いたり、他のオブジェクトの状態を大幅に変更したりすることは避けるべきです。

利用者は const x = user.name; という記述が非常に軽量であることを期待しています。

重い処理が含まれる場合は、メソッド名に fetch...compute... といった接頭辞を付け、明示的に関数として定義する方が親切です。

2026年以降のトレンド:デコレータとの連携

TypeScript 5.0以降で正式にサポートされた「デコレータ」を活用することで、アクセサーの振る舞いをさらにスマートに拡張できるようになっています。

例えば、特定のsetterに対して自動的にログを出力したり、バリデーションロジックを注入したりするコードが、より簡潔に記述できるようになりました。

今後の開発では、標準のアクセサーとデコレータを組み合わせたメタプログラミングの手法がさらに普及していくと予想されます。

まとめ

TypeScriptのgetterとsetterは、クラス設計におけるカプセル化を強力にサポートする機能です。

適切に使用することで、データの整合性を保ち、外部からの予期せぬ操作を制限することができます。

今回のポイントを振り返ります。

  • アクセサーは内部変数(_#)へのアクセスを制御するブリッジである。
  • バリデーションや計算済みプロパティの実装に最適である。
  • 読み書きはプロパティ形式で行えるため、利用側のコードがスッキリする。
  • 重い処理や非同期処理はメソッドとして実装し、アクセサーには含めない。

これらのルールを意識してアクセサーを使いこなし、美しくメンテナンス性の高いTypeScriptコードを目指しましょう。

カプセル化を正しく実装することは、長期的なプロジェクトの成功に大きく貢献するはずです。