ゴール:「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 が呼ばれる)
TypeScriptsetter の型のポイントは、
- 戻り値の型は書かない(常に
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: numberはprivate - 外から見えるのは
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ここでの設計はこうです。
_idはprivate 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: 型) の「型」を意識して書くと、
“プロパティの顔をした関数”としてのイメージが、
かなりクリアになるはずです。
