TypeScript | 関数・クラス・ジェネリクス:クラス設計 – getter / setter の型

TypeScript TypeScript
スポンサーリンク

ゴール:「getter / setter の“型”を見て、どう使うかイメージできるようになる」

getter / setter は、クラスのプロパティに「振る舞い」をくっつける仕組みです。
でも、ただ「便利そうだから使う」だと、だいたいごちゃつきます。

ここでは、

  • getter / setter の基本構文と型の考え方
  • 「プロパティっぽく見えるけど実は関数」という感覚
  • 型をどう設計すると読みやすくなるか
  • 実務でよくある使い方と注意点

を、初心者向けにかみ砕いて整理していきます。


基本:getter / setter は「プロパティに見える関数」

getter の型の基本イメージ

まずは getter から。

class User {
  constructor(
    private firstName: string,
    private lastName: string
  ) {}

  get fullName(): string {
    return `${this.lastName} ${this.firstName}`;
  }
}

const u = new User("Taro", "Yamada");
console.log(u.fullName); // "Yamada Taro"
TypeScript

ここで大事なポイントは、2つです。

1つ目は、get fullName(): string: string
これは「この getter は string を返す」という意味で、
普通のメソッドの戻り値の型と同じ考え方です。

2つ目は、呼び出し方。
u.fullName() ではなく、u.fullName と「プロパティのように」アクセスします。

つまり、型のイメージとしては、

  • 宣言側では「戻り値の型を持つ関数」
  • 利用側では「その型のプロパティ」

という、ちょっとおいしい存在です。

setter の型の基本イメージ

次に setter。

class User {
  private _age: number = 0;

  get age(): number {
    return this._age;
  }

  set age(value: number) {
    if (value < 0) {
      throw new Error("年齢は0以上である必要があります");
    }
    this._age = value;
  }
}

const u = new User();
u.age = 20;          // OK(setter が呼ばれる)
console.log(u.age);  // 20(getter が呼ばれる)
TypeScript

setter の型のポイントは、

  • 戻り値の型は書かない(常に void 扱い)
  • 引数の型だけを指定する(ここでは value: number

というところです。

宣言としては「引数を1つ取るメソッド」ですが、
利用側からは u.age = 20 という「代入」に見えます。

getter / setter は、

  • getter:get name(): 型(戻り値の型を指定)
  • setter:set name(value: 型)(引数の型を指定)

という形で、「プロパティの型」を挟んでいるイメージを持つと理解しやすいです。


getter / setter と「内部状態」の関係

内部の生データを隠して、意味のある形で見せる

よくあるパターンは、「内部の状態はそのまま見せたくないけど、加工した形は見せたい」というケースです。

class Price {
  constructor(
    private amount: number,     // 税抜き金額
    private taxRate: number     // 例: 0.1(10%)
  ) {}

  get withTax(): number {
    return Math.floor(this.amount * (1 + this.taxRate));
  }
}

const p = new Price(1000, 0.1);
console.log(p.withTax); // 1100
TypeScript

ここでの型の設計はこうです。

  • 内部状態 amount: number, taxRate: numberprivate
  • 外から見えるのは withTax: number という「計算済みの値」

getter の型 : number は、
「このプロパティにアクセスすると、常に number が返ってくる」
という約束を表しています。

「内部の構造は隠して、意味のある値だけを見せたい」とき、
getter の型は「外から見える世界」を定義するものになります。

setter で「値の制約」を型+ロジックで表現する

さきほどの age の例を、もう少し意識して見てみます。

class User {
  private _age: number = 0;

  get age(): number {
    return this._age;
  }

  set age(value: number) {
    if (value < 0) {
      throw new Error("年齢は0以上である必要があります");
    }
    this._age = value;
  }
}
TypeScript

型としては value: number ですが、
ロジックとしては「0以上」という制約を加えています。

ここで重要なのは、

  • 型だけでは「0以上」を表現しづらい
  • でも setter を通すことで、「代入のたびにチェックする」ことができる

という点です。

getter / setter は、

  • 型:ざっくりとした「値の種類」(number, string など)
  • ロジック:より細かい「値の制約」(0以上、空文字禁止など)

を組み合わせて、「安全なプロパティアクセス」を作る道具だと捉えると、使いどころが見えてきます。


getter / setter と readonly / private の組み合わせ

「外からは読み取りだけOK」にしたいとき

例えば、「ID は外から見えるけど、絶対に書き換えられたくない」ケース。

class User {
  private readonly _id: number;
  private _name: string;

  constructor(id: number, name: string) {
    this._id = id;
    this._name = name;
  }

  get id(): number {
    return this._id;
  }

  get name(): string {
    return this._name;
  }

  set name(value: string) {
    if (value.length === 0) {
      throw new Error("名前は空にできません");
    }
    this._name = value;
  }
}

const u = new User(1, "Taro");
console.log(u.id);   // OK(getter)
console.log(u.name); // OK(getter)
u.name = "Jiro";     // OK(setter)
// u.id = 2;         // そもそも setter がないのでコンパイルエラー
TypeScript

ここでの設計はこうです。

  • _idprivate readonly(内部でも再代入不可)
  • id は getter だけ(外からは読み取り専用)
  • name は getter + setter(外から読み書き可能、ただし制約付き)

getter / setter の有無と、内部フィールドの readonly / private を組み合わせることで、

  • 完全に不変(読み取り専用)
  • 制約付きで変更可能
  • 完全に隠蔽(外から見えない)

といったバリエーションを作れます。


getter / setter の型と「プロパティとしての顔」

「プロパティの型」として意識する

TypeScript 的には、getter / setter を定義すると、
その名前の「プロパティの型」が決まります。

例えば、さきほどの User クラスで name の型を見てみると、
外からは「string 型のプロパティ」として扱われます。

class User {
  private _name: string;

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

  get name(): string {
    return this._name;
  }

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

const u = new User("Taro");

// ここでの推論
const n: string = u.name; // OK
u.name = "Jiro";          // OK
TypeScript

つまり、getter / setter の型は、

  • getter の戻り値の型
  • setter の引数の型

が一致している限り、
「そのプロパティの型」として扱われます。

ここが崩れるとおかしくなるので、
基本的には「getter と setter で同じ型を使う」と覚えておいてください。

getter だけ定義した場合の型

getter だけ定義して、setter を定義しない場合は、
そのプロパティは「読み取り専用」として扱われます。

class User {
  private _createdAt: Date = new Date();

  get createdAt(): Date {
    return this._createdAt;
  }
}

const u = new User();
const d: Date = u.createdAt; // OK
// u.createdAt = new Date(); // エラー:書き込み不可
TypeScript

ここでは、

  • プロパティの型:Date
  • アクセス可能:読み取りのみ

という状態です。

「外からは読み取りだけさせたい」プロパティは、
getter だけ定義する、というのが素直な設計になります。


実務でよくある getter / setter の使いどころ

「計算済みの値」をプロパティっぽく見せたいとき

例えば、ユーザーのフルネーム、税込み価格、合計金額など、
「内部状態から計算される値」をプロパティとして見せたいとき。

class CartItem {
  constructor(
    public price: number,
    public quantity: number
  ) {}

  get total(): number {
    return this.price * this.quantity;
  }
}

const item = new CartItem(1000, 3);
console.log(item.total); // 3000
TypeScript

ここで total をメソッドにして item.total() としてもいいのですが、
「状態に紐づく“属性”」として扱いたいなら、
getter でプロパティにしてしまうと読みやすくなります。

型としては get total(): number
「このプロパティにアクセスすると、常に number が返る」という約束です。

「代入のたびに検証したい」プロパティ

例えば、メールアドレスやパスワードなど、
「適当な値を入れられたら困る」プロパティ。

class Account {
  private _email: string = "";

  get email(): string {
    return this._email;
  }

  set email(value: string) {
    if (!value.includes("@")) {
      throw new Error("メールアドレスの形式が不正です");
    }
    this._email = value;
  }
}

const a = new Account();
a.email = "test@example.com"; // OK
// a.email = "invalid";       // 実行時にエラー
TypeScript

型としては value: string ですが、
ロジックで「メールアドレスっぽさ」をチェックしています。

「型だけでは表現しきれない制約」を、
setter の中に閉じ込める、という使い方です。


まとめ:getter / setter の型を自分の言葉で整理すると

最後に、あなた自身の言葉でこう整理してみてください。

getter / setter は、

  • 宣言側では「戻り値型(getter)」「引数型(setter)を持つメソッド」
  • 利用側では「その型のプロパティ」として振る舞う

という存在。

設計するときは、

  • getter の戻り値の型 = プロパティの型
  • setter の引数の型 = プロパティに代入できる型
  • getter と setter で型を揃える(基本ルール)
  • getter だけ定義すれば「読み取り専用プロパティ」になる
  • 内部フィールドの private / readonly と組み合わせて、「何を見せて、何を隠すか」を決める

今書いているクラスの中から、
「本当は計算済みの値として見せたいもの」や
「代入のたびにチェックしたいプロパティ」を1つ選んで、
それを getter / setter にしてみてください。

そのとき、
get ...(): 型set ...(value: 型) の「型」を意識して書くと、
“プロパティの顔をした関数”としてのイメージが、
かなりクリアになるはずです。

タイトルとURLをコピーしました