TypeScript:クラスとアクセス修飾子
1学習の目的
- クラスに型とアクセス修飾子を付けられるようになる。JS14で学んだクラスが、TypeScriptでは外部から触れる範囲を型で制御できるようになる。
- abstract と implements を使い、設計の約束事をコードに埋め込めるようになる。「この機能は必ず実装すること」を型で強制できる。
2基礎解説
| 修飾子 | 意味 | 外部から |
|---|---|---|
| public | 公開(既定) | 触れる |
| private | クラス内だけ | 触れない |
| readonly | 読み取り専用 | 読めるが変更不可 |
| protected | 継承先まで | 触れない |
- コンストラクタの引数に修飾子を付けると、プロパティが自動で作られる。constructor(public name: string) だけで this.name が使えるようになり、記述が大幅に減る。
- private は実行前にエラーになる。JS14の # は実行時のエラーだったが、TypeScriptの private は書いた時点で止められる。
- readonly は読めるが変更できない。設定値やIDなど、後から変わってほしくないものに使う。
- abstract は「継承先で必ず実装せよ」という指示。実装を忘れるとエラーになるので、実装漏れが起きない。
- 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問)
出力 == ノート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特有の便利な記法。
A. undefined が返る B. 実行前にエラーになる C. 実行時にエラーになる D. 問題ない 選択★☆☆無料
解答 == B
TypeScriptの型チェックはいつ行われるか。
B
JS14の # は実行時エラーだったが、private は書いた時点で分かる。エディタ上で赤線が引かれるので、実行する前に気づける。
出力 == 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 にすると、外部からの直接アクセスを禁じられる。取り出す手段をメソッドに限定することで、値の扱い方をクラスが管理できる。
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" と書くとエラーになるので、設定値の書き換え事故を防げる。
A. 何も起きない B. エラーになる C. 空の処理になる D. 親の処理が使われる 選択★☆☆無料
解答 == B
abstract は「必ず実装せよ」という指示。
B
実装漏れが構造的に防げる。「このクラスを継承するなら、この処理は必ず用意すること」という約束を、型として強制できる。
出力 == 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で学んだプライベートフィールドが、ここでも役立つ。
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 を書き忘れればエラーになるので、実装漏れが防げる。
A. 引数を受け取るだけ B. name プロパティが自動で作られる C. エラーになる D. name が読み取り専用になる 選択★★☆無料
解答 == B
TypeScript特有の省略記法。
B
プロパティの宣言と代入が同時に行われる。name: string; と this.name = name; の2行を書かずに済む。
出力 == 面積: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で学んだポリモーフィズム。
出力 == 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問)
出力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 は外から触れない——それぞれの性質に応じた保護をかけている。
出力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の型ガードがここで必要になる。
出力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[] として扱えるのがポリモーフィズムの本質。ループの中では円か長方形かを気にせず、同じ書き方で処理できる。
出力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());
}
継承関係が無くても、同じインターフェースを実装していれば同じ配列に入れられる。「何であるか」ではなく「何ができるか」で型を揃える考え方。
出力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仕上げ課題
作るクラス
① abstract class Product
・readonly id、readonly name、protected 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 price。private だと子クラスから使えないが、protected なら継承先では使える。外部には隠しつつ、継承関係の内側では共有する——この中間的な保護が表現できる。
第三に get items(): readonly Line[] だ。内部の配列をそのまま返すと、外部から order.items.push(…) と直接追加されてしまう。readonly を付けることで読めるが変更できない状態にし、追加は必ず add を通るよう強制している。
クラス設計とは「何を公開し、何を隠すか」を決めることだ。TypeScriptの修飾子は、その判断をコードに刻み込む道具になる。
次章ではユーティリティ型を学ぶ。既存の型から「一部だけ」「全部省略可能」といった新しい型を作れるようになる。