TypeScript / LESSON 12 / TS12

TypeScript:クラスとアクセス修飾子

1学習の目的

2基礎解説

修飾子意味外部から
public公開(既定)触れる
privateクラス内だけ触れない
readonly読み取り専用読めるが変更不可
protected継承先まで触れない
✅ 覚えるべき重要ポイント
  1. コンストラクタの引数に修飾子を付けると、プロパティが自動で作られるconstructor(public name: string) だけで this.name が使えるようになり、記述が大幅に減る。
  2. private は実行前にエラーになる。JS14の # は実行時のエラーだったが、TypeScriptの private は書いた時点で止められる
  3. readonly読めるが変更できない。設定値やIDなど、後から変わってほしくないものに使う。
  4. abstract は「継承先で必ず実装せよ」という指示。実装を忘れるとエラーになるので、実装漏れが起きない。
  5. implements はインターフェースとの契約。「この型が要求するメソッドを持つ」ことを保証し、足りなければエラーになる。

現場使用例:APIクライアント、状態を持つサービス、ドメインモデル、Errorを継承した自作エラー。「データと処理をまとめ、外から触れる範囲を絞る」設計の中心

// コンストラクタで修飾子を書くとプロパティが自動生成される
class Product {
  constructor(
    public readonly id: number,
    public name: string,
    private price: number
  ) {}

  // private なプロパティはクラス内からは使える
  getPrice(): number {
    return this.price;
  }
}

const p = new Product(1, "ノートPC", 128000);
console.log(p.name);        // OK(public)
// console.log(p.price);    // ❌ private なのでエラー
// p.id = 2;                // ❌ readonly なので変更不可

// abstract:継承先で必ず実装させる
abstract class Shape {
  abstract area(): number;          // 実装は子に任せる

  describe(): string {              // 共通処理は親に書ける
    return `面積:${this.area()}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }
  area(): number {                  // 実装しないとエラー
    return this.radius ** 2 * 3.14;
  }
}

// implements:インターフェースとの契約
interface Printable {
  print(): string;
}

class Item implements Printable {
  constructor(private name: string) {}
  print(): string {                 // 実装しないとエラー
    return `[${this.name}]`;
  }
}

3基本ドリル(10問)

TS12-D01 名前と価格を持つ Product クラスを定義し(コンストラクタの引数に public を付けて)、1件作って「ノートPC:128,000円」と出力せよ。 コーディング★☆☆無料
期待される結果

出力 == ノートPC:128,000円

ヒント

constructor(public name: string, public price: number) {} と書くだけでプロパティが作られる。

模範解答
class Product {
  constructor(
    public name: string,
    public price: number
  ) {}
}

const p = new Product("ノートPC", 128000);

console.log(`${p.name}:${p.price.toLocaleString()}円`);
解説

this.name = name; を書く必要がない。修飾子を付けるだけでプロパティの宣言と代入が自動で行われる——TypeScript特有の便利な記法。

TS12-D02 private なプロパティにクラスの外からアクセスするとどうなるか。
A. undefined が返る B. 実行前にエラーになる C. 実行時にエラーになる D. 問題ない
選択★☆☆無料
期待される結果

解答 == B

ヒント

TypeScriptの型チェックはいつ行われるか。

模範解答
B
解説

JS14の # は実行時エラーだったが、private は書いた時点で分かる。エディタ上で赤線が引かれるので、実行する前に気づける。

TS12-D03 価格を private で持つ Product クラスを定義し、価格を返すメソッド getPrice を経由して出力せよ(価格128000)。 コーディング★☆☆無料
期待される結果

出力 == 128000

ヒント

private price: number とし、getPrice(): number { return this.price; } を作る。

模範解答
class Product {
  constructor(private price: number) {}

  getPrice(): number {
    return this.price;
  }
}

console.log(new Product(128000).getPrice());
解説

private にすると、外部からの直接アクセスを禁じられる。取り出す手段をメソッドに限定することで、値の扱い方をクラスが管理できる。

TS12-D04 変更できないプロパティを定義するコードを完成させよ。 穴埋め★☆☆無料
コード
class Config {
  constructor(____ apiUrl: string) {}
}

const c = new Config("https://example.com");
console.log(c.apiUrl);
期待される結果

出力 == https://example.com

ヒント

「読み取り専用」を表す8文字のキーワード。

模範解答
readonly
解説

readonly は読めるが変更できないc.apiUrl = "別のURL" と書くとエラーになるので、設定値の書き換え事故を防げる。

TS12-D05 abstract なメソッドを、継承先で実装しなかったらどうなるか。
A. 何も起きない B. エラーになる C. 空の処理になる D. 親の処理が使われる
選択★☆☆無料
期待される結果

解答 == B

ヒント

abstract は「必ず実装せよ」という指示。

模範解答
B
解説

実装漏れが構造的に防げる。「このクラスを継承するなら、この処理は必ず用意すること」という約束を、型として強制できる。

TS12-D06 残高を private で持つ Account クラスを定義し、入金メソッド deposit と残高を返す getter balance を作れ。1000円で開始し500円入金した結果を出力すること。 コーディング★★☆無料
期待される結果

出力 == 1500

ヒント

private balance: number とし、get balance() は名前が衝突するので内部は別名にするか # を使う。

模範解答
class Account {
  #balance: number;

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

  get balance(): number {
    return this.#balance;
  }

  deposit(amount: number): void {
    this.#balance += amount;
  }
}

const account = new Account(1000);
account.deposit(500);

console.log(account.balance);
解説

getter の名前とプロパティ名が衝突するので # を使っている。JS14で学んだプライベートフィールドが、ここでも役立つ。

TS12-D07 インターフェースの実装を宣言するキーワードを補ってコードを完成させよ。 穴埋め★★☆無料
コード
interface Printable {
  print(): string;
}

class Item ____ Printable {
  constructor(private name: string) {}
  print(): string {
    return `[${this.name}]`;
  }
}

console.log(new Item("A").print());
期待される結果

出力 == [A]

ヒント

「実装する」を意味する10文字のキーワード。

模範解答
implements
解説

implements は「この型の要求を満たす」という宣言。print を書き忘れればエラーになるので、実装漏れが防げる。

TS12-D08 constructor(public name: string) と書くと何が起きるか。
A. 引数を受け取るだけ B. name プロパティが自動で作られる C. エラーになる D. name が読み取り専用になる
選択★★☆無料
期待される結果

解答 == B

ヒント

TypeScript特有の省略記法。

模範解答
B
解説

プロパティの宣言と代入が同時に行われるname: string;this.name = name; の2行を書かずに済む。

TS12-D09 abstract クラス Shape(area は abstract、describe は共通実装)を定義し、Circle(半径10)で継承して「面積:314」と出力せよ(円周率3.14)。 コーディング★★☆無料
期待される結果

出力 == 面積:314

ヒント

abstract class Shape { abstract area(): number; describe(): string {...} } と書く。

模範解答
abstract class Shape {
  abstract area(): number;

  describe(): string {
    return `面積:${this.area()}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  area(): number {
    return this.radius ** 2 * 3.14;
  }
}

console.log(new Circle(10).describe());
解説

共通処理は親に、固有の計算は子に。describe は area の中身を知らないまま呼び出せる——これがJS14で学んだポリモーフィズム。

TS12-D10 static なカウンタを持つ Counter クラスを定義し、increment を3回呼んだ結果を出力せよ。 コーディング★★☆無料
期待される結果

出力 == 3

ヒント

static count = 0; と static increment() を定義する。インスタンス化せずに使える。

模範解答
class Counter {
  static count = 0;

  static increment(): number {
    return ++Counter.count;
  }
}

Counter.increment();
Counter.increment();
Counter.increment();

console.log(Counter.count);
解説

static はクラス全体で共有される。インスタンスごとではなく、クラスに1つだけ存在する値になる。

4実践シナリオ(5問)

TS12-S01 【商品クラス】id(readonly)・name(public)・price(private)を持つ Product クラスを定義し、税込価格を返すメソッド withTax を作れ。2件作り、「ノートPC:140,800円」の形式で2行出力すること(ノートPC/128000、マウス/3200)。 コーディング★★☆無料
期待される結果

出力2行が完全一致

ヒント

withTax は Math.round(this.price * 1.1) を返す。

模範解答
class Product {
  constructor(
    public readonly id: number,
    public name: string,
    private price: number
  ) {}

  withTax(): number {
    return Math.round(this.price * 1.1);
  }
}

const items = [
  new Product(1, "ノートPC", 128000),
  new Product(2, "マウス", 3200),
];

for (const item of items) {
  console.log(`${item.name}:${item.withTax().toLocaleString()}円`);
}
解説

3つの修飾子を使い分けている。id は変えられず、name は自由に読み書きでき、price は外から触れない——それぞれの性質に応じた保護をかけている。

TS12-S02 【銀行口座】残高を # で持つ Account クラスを定義し、出金メソッド withdraw(残高不足なら例外を投げる)を作れ。10000円で開始し、3000円出金→99999円出金(失敗)を試し、「残高:7,000円」「エラー:残高が不足しています」と2行出力すること。 コーディング★★☆無料
期待される結果

出力2行が完全一致

ヒント

throw new Error(...) を使い、呼び出し側で try/catch する(JS10)。

模範解答
class Account {
  #balance: number;

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

  get balance(): number {
    return this.#balance;
  }

  withdraw(amount: number): void {
    if (amount > this.#balance) {
      throw new Error("残高が不足しています");
    }
    this.#balance -= amount;
  }
}

const account = new Account(10000);

account.withdraw(3000);
console.log(`残高:${account.balance.toLocaleString()}円`);

try {
  account.withdraw(99999);
} catch (e) {
  console.log(`エラー:${e instanceof Error ? e.message : "不明"}`);
}
解説

catch で受け取る値は unknown 型(TypeScriptの厳格な仕様)。e.message と直接書けないので、e instanceof Error で確認してから使う——TS09の型ガードがここで必要になる。

TS12-S03 【図形の面積】abstract クラス Shape を定義し、Circle(半径10)と Rectangle(幅5・高さ4)で継承せよ。配列にまとめて「円:314.0」「長方形:20.0」の形式で2行出力すること。 コーディング★★☆無料
期待される結果

出力2行。円:314.0 / 長方形:20.0

ヒント

abstract に area() と name を持たせる。配列の型は Shape[]。

模範解答
abstract class Shape {
  abstract readonly label: string;
  abstract area(): number;
}

class Circle extends Shape {
  readonly label = "円";
  constructor(private radius: number) {
    super();
  }
  area(): number {
    return this.radius ** 2 * 3.14;
  }
}

class Rectangle extends Shape {
  readonly label = "長方形";
  constructor(private width: number, private height: number) {
    super();
  }
  area(): number {
    return this.width * this.height;
  }
}

const shapes: Shape[] = [new Circle(10), new Rectangle(5, 4)];

for (const s of shapes) {
  console.log(`${s.label}:${s.area().toFixed(1)}`);
}
解説

Shape[] として扱えるのがポリモーフィズムの本質。ループの中では円か長方形かを気にせず、同じ書き方で処理できる。

TS12-S04 【インターフェースの契約】Printable(print を持つ)を定義し、Product と Customer の2クラスで implements せよ。両方を配列にまとめて2行出力すること(「[商品] ノートPC」「[顧客] 田中商事」)。 コーディング★★★無料
期待される結果

出力2行が完全一致

ヒント

配列の型を Printable[] にすると、異なるクラスを同じ配列に入れられる。

模範解答
interface Printable {
  print(): string;
}

class Product implements Printable {
  constructor(private name: string) {}
  print(): string {
    return `[商品] ${this.name}`;
  }
}

class Customer implements Printable {
  constructor(private name: string) {}
  print(): string {
    return `[顧客] ${this.name}`;
  }
}

const items: Printable[] = [
  new Product("ノートPC"),
  new Customer("田中商事"),
];

for (const item of items) {
  console.log(item.print());
}
解説

継承関係が無くても、同じインターフェースを実装していれば同じ配列に入れられる。「何であるか」ではなく「何ができるか」で型を揃える考え方。

TS12-S05 【在庫管理クラス】商品を追加・検索できる Inventory クラスを定義せよ。商品配列は private とし、add・findByName・totalValue(在庫総額)のメソッドを持つこと。2件追加して「登録2件」「マウス:3,200円」「総額:262,400円」を3行出力すること(ノートPC/128000/1、マウス/3200/42)。 コーディング★★★無料
期待される結果

出力3行が完全一致

ヒント

private items: Product[] = []; を持ち、メソッドで操作する。totalValue は price * stock の合計。

模範解答
type Product = { name: string; price: number; stock: number };

class Inventory {
  private items: Product[] = [];

  add(item: Product): void {
    this.items.push(item);
  }

  get count(): number {
    return this.items.length;
  }

  findByName(name: string): Product | undefined {
    return this.items.find((i) => i.name === name);
  }

  totalValue(): number {
    return this.items.reduce((sum, i) => sum + i.price * i.stock, 0);
  }
}

const inv = new Inventory();
inv.add({ name: "ノートPC", price: 128000, stock: 1 });
inv.add({ name: "マウス", price: 3200, stock: 42 });

console.log(`登録${inv.count}件`);

const found = inv.findByName("マウス");
console.log(
  found ? `${found.name}:${found.price.toLocaleString()}円` : "該当なし"
);

console.log(`総額:${inv.totalValue().toLocaleString()}円`);
解説

配列を private にすることで、外部から直接 push されない。追加は必ず add を通るので、将来「重複チェック」などの処理を足したくなったら1箇所直すだけで済む。

5仕上げ課題

TS12-FINAL 【ECサイトの注文システム】クラス設計で注文処理を実装せよ。

作るクラス
abstract class Product
 ・readonly idreadonly nameprotected price
 ・abstract get label(): string(種別名)
 ・abstract needsShipping(): boolean
 ・subtotal(qty: number): number(共通実装:price × qty)

DigitalProduct extends Product … label は「電子」、送料不要
PhysicalProduct extends Product … label は「物理」、送料必要

class Order
 ・private lines(商品と数量の配列)
 ・add(product, qty)get subtotal()get shipping()(送料必要な商品があれば800、なければ0)、get total()

処理:ノートPC(物理/128000)×1、eBook(電子/2400)×2 を注文

期待される出力(5行)
[物理] ノートPC × 1 = 128,000円
[電子] eBook × 2 = 4,800円
小計:132,800円
送料:800円
合計:133,600円
期待される結果

出力5行が完全一致

ヒント

abstract クラスに共通処理(subtotal)を書き、label と needsShipping は子に実装させる。Order の lines は private にし、add で追加する。shipping は some で判定する。

模範解答
abstract class Product {
  constructor(
    public readonly id: number,
    public readonly name: string,
    protected price: number
  ) {}

  abstract get label(): string;
  abstract needsShipping(): boolean;

  subtotal(qty: number): number {
    return this.price * qty;
  }
}

class DigitalProduct extends Product {
  get label(): string {
    return "電子";
  }
  needsShipping(): boolean {
    return false;
  }
}

class PhysicalProduct extends Product {
  get label(): string {
    return "物理";
  }
  needsShipping(): boolean {
    return true;
  }
}

type Line = { product: Product; qty: number };

class Order {
  private lines: Line[] = [];

  add(product: Product, qty: number): void {
    this.lines.push({ product, qty });
  }

  get items(): readonly Line[] {
    return this.lines;
  }

  get subtotal(): number {
    return this.lines.reduce(
      (sum, l) => sum + l.product.subtotal(l.qty),
      0
    );
  }

  get shipping(): number {
    return this.lines.some((l) => l.product.needsShipping()) ? 800 : 0;
  }

  get total(): number {
    return this.subtotal + this.shipping;
  }
}

const order = new Order();
order.add(new PhysicalProduct(1, "ノートPC", 128000), 1);
order.add(new DigitalProduct(2, "eBook", 2400), 2);

for (const line of order.items) {
  console.log(
    `[${line.product.label}] ${line.product.name} × ${line.qty} = ${line.product
      .subtotal(line.qty)
      .toLocaleString()}円`
  );
}

console.log(`小計:${order.subtotal.toLocaleString()}円`);
console.log(`送料:${order.shipping.toLocaleString()}円`);
console.log(`合計:${order.total.toLocaleString()}円`);
解説

JS14で書いた注文システムに、型による保護が加わった。変化した点を3つ挙げる。

第一に abstract だ。JS14では「子クラスで label を実装し忘れる」可能性があったが、TypeScriptでは実装しなければコンパイルが通らない。設計上の約束が、型として強制されている。

第二に protected priceprivate だと子クラスから使えないが、protected なら継承先では使える。外部には隠しつつ、継承関係の内側では共有する——この中間的な保護が表現できる。

第三に get items(): readonly Line[] だ。内部の配列をそのまま返すと、外部から order.items.push(…) と直接追加されてしまう。readonly を付けることで読めるが変更できない状態にし、追加は必ず add を通るよう強制している。

クラス設計とは「何を公開し、何を隠すか」を決めることだ。TypeScriptの修飾子は、その判断をコードに刻み込む道具になる。

次章ではユーティリティ型を学ぶ。既存の型から「一部だけ」「全部省略可能」といった新しい型を作れるようになる。