メインコンテンツまでスキップ

クラスの基本

この章では、TypeScriptにおけるクラスの基本的な書き方を学びます。

この章で学ぶこと

  • クラスの定義方法
  • コンストラクタの使い方
  • アクセス修飾子(public, private, protected)
  • コンストラクタの短縮記法
  • ゲッターとセッター
備考

この章のコード例は、TypeScript Playgroundで試すことができます。

クラスとは

クラスは、オブジェクトの「設計図」です。同じ構造を持つオブジェクトを何度でも作成できます。

オブジェクトリテラルとクラスの違い

// オブジェクトリテラル: 1つのオブジェクトを直接作成
const person1 = {
name: 'Alice',
age: 25,
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};

// 同じ構造のオブジェクトを作るには、毎回書く必要がある
const person2 = {
name: 'Bob',
age: 30,
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};

クラスを使うと、同じ構造のオブジェクトを簡単に作成できます。

// クラス: オブジェクトの設計図
class Person {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

greet(): void {
console.log(`Hello, I'm ${this.name}`);
}
}

// クラスからインスタンス(実体)を生成
const person1 = new Person('Alice', 25);
const person2 = new Person('Bob', 30);

person1.greet(); // 'Hello, I'm Alice'
person2.greet(); // 'Hello, I'm Bob'

クラスの定義

基本的な構文

class クラス名 {
// プロパティ(フィールド)
プロパティ名:;

// コンストラクタ
constructor(パラメータ:) {
this.プロパティ名 = パラメータ;
}

// メソッド
メソッド名(): 戻り値の型 {
// 処理
}
}

具体例: Userクラス

class User {
// プロパティの宣言
name: string;
email: string;
age: number;

// コンストラクタ: インスタンス生成時に呼ばれる
constructor(name: string, email: string, age: number) {
// thisはこれから作成されるインスタンス自身を指す
this.name = name;
this.email = email;
this.age = age;
}

// メソッドの定義
introduce(): void {
console.log(`Name: ${this.name}, Email: ${this.email}, Age: ${this.age}`);
}

// 戻り値のあるメソッド
isAdult(): boolean {
return this.age >= 18;
}
}

// インスタンスの生成
const user1 = new User('Alice', 'alice@example.com', 25);
const user2 = new User('Bob', 'bob@example.com', 16);

// メソッドの呼び出し
user1.introduce(); // 'Name: Alice, Email: alice@example.com, Age: 25'
console.log(user1.isAdult()); // true
console.log(user2.isAdult()); // false

コード解説:

  • name: string; - プロパティの型宣言(クラス内で必須)
  • constructor(...) - インスタンス生成時に自動的に呼ばれる特別なメソッド
  • this.name = name - thisはインスタンス自身を指す
  • new User(...) - クラスからインスタンスを生成

プロパティの初期値

プロパティに直接初期値も設定できます。

class Counter {
// プロパティに直接初期値を設定
count: number = 0;
step: number = 1;

increment(): void {
this.count += this.step;
}

getCount(): number {
return this.count;
}
}

const counter = new Counter();
console.log(counter.getCount()); // 0
counter.increment();
console.log(counter.getCount()); // 1

コンストラクタ

コンストラクタは、newでインスタンスを生成する際に自動的に呼ばれる特別なメソッドです。

class Product {
name: string;
price: number;
category: string;

constructor(name: string, price: number, category: string = 'general') {
this.name = name;
this.price = price;
this.category = category; // デフォルト値も設定可能
}

getInfo(): string {
return `${this.name}: ¥${this.price} (${this.category})`;
}
}

const product1 = new Product('Laptop', 100000, 'electronics');
const product2 = new Product('Notebook', 500); // categoryはデフォルト値

console.log(product1.getInfo()); // 'Laptop: ¥100000 (electronics)'
console.log(product2.getInfo()); // 'Notebook: ¥500 (general)'

アクセス修飾子

アクセス修飾子を使うと、プロパティやメソッドへのアクセス範囲を制御できます。

public(公開)

デフォルトの修飾子で、どこからでもアクセスできます。

class User {
// publicは省略可能(デフォルトでpublic)
public name: string;
public age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

public greet(): void {
console.log(`Hello, I'm ${this.name}`);
}
}

const user = new User('Alice', 25);

// 外部から自由にアクセス可能
console.log(user.name); // 'Alice'
console.log(user.age); // 25
user.greet(); // 'Hello, I'm Alice'

// プロパティの変更も可能
user.name = 'Alicia';
user.age = 26;

private(非公開)

クラス内部でのみアクセスできます。外部や継承先からもアクセスできません。

class BankAccount {
public accountNumber: string;
private balance: number; // 外部からアクセスできない

constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}

// 残高を取得するpublicメソッド
public getBalance(): number {
return this.balance;
}

// 入金
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
console.log(`Deposited ${amount}. New balance: ${this.balance}`);
}
}

// 出金
public withdraw(amount: number): boolean {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
console.log(`Withdrawn ${amount}. New balance: ${this.balance}`);
return true;
}
console.log('Insufficient funds');
return false;
}
}

const account = new BankAccount('123-456', 1000);

// OK: publicなのでアクセス可能
console.log(account.accountNumber); // '123-456'

// Error: privateなので外部からアクセス不可
// console.log(account.balance);
// Property 'balance' is private and only accessible within class 'BankAccount'

// OK: publicメソッド経由でのみアクセス可能
console.log(account.getBalance()); // 1000
account.deposit(500); // 'Deposited 500. New balance: 1500'
account.withdraw(300); // 'Withdrawn 300. New balance: 1200'

privateの用途:

  • 外部から直接変更してほしくないデータを保護
  • クラスの内部実装を隠蔽(カプセル化)
  • データの整合性を保つ

protected(保護)

クラス内部と継承先クラスからアクセスできます。外部からはアクセスできません。

class Person {
protected name: string; // 継承先からもアクセス可能
protected age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

protected getInfo(): string {
return `${this.name} (${this.age})`;
}
}

class Employee extends Person {
private employeeId: string;

constructor(name: string, age: number, employeeId: string) {
super(name, age); // 親クラスのコンストラクタを呼び出し
this.employeeId = employeeId;
}

// protectedプロパティにアクセス可能
public introduce(): void {
console.log(`I'm ${this.name}, ${this.age} years old. ID: ${this.employeeId}`);
}
}

const employee = new Employee('Alice', 30, 'E001');
employee.introduce(); // 'I'm Alice, 30 years old. ID: E001'

// Error: protectedなので外部からアクセス不可
// console.log(employee.name);
// console.log(employee.age);

ECMAScript Private Fields(#プロパティ)

TypeScriptのprivate修飾子に加えて、JavaScript標準の#プレフィックスを使ったプライベートフィールドも使用できます。

class BankAccount {
// ECMAScript Private Field(#で始まる)
#balance: number;
accountNumber: string;

constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.#balance = initialBalance;
}

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

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

const account = new BankAccount("123-456", 1000);
console.log(account.getBalance()); // 1000

// #balanceは外部からアクセス不可
// console.log(account.#balance);
// Error: Property '#balance' is not accessible outside class 'BankAccount'
// because it has a private identifier.

TypeScriptのprivateと#の違い:

特徴private# (Private Field)
アクセス制限コンパイル時のみ実行時も有効
JavaScript出力通常のプロパティ#プレフィックス保持
継承時のアクセスサブクラスからアクセス不可サブクラスからアクセス不可
外部からの強制アクセス(obj as any).propで可能完全に不可能
// 違いの例
class Example {
private tsPrivate = "TypeScript private";
#jsPrivate = "JavaScript private";
}

const ex = new Example();
// (ex as any).tsPrivate // "TypeScript private"(実行時はアクセス可能)
// (ex as any).#jsPrivate // SyntaxError(実行時もアクセス不可)
備考

セキュリティが重要な場合やライブラリ開発では#を推奨。通常のアプリケーション開発ではprivateで十分な場合が多いです。

readonly(読み取り専用)

初期化後に変更できないプロパティを定義します。

class Product {
public readonly id: number;
public readonly name: string;
public price: number; // 変更可能

constructor(id: number, name: string, price: number) {
// コンストラクタ内では代入可能
this.id = id;
this.name = name;
this.price = price;
}

updatePrice(newPrice: number): void {
this.price = newPrice; // OK: readonlyではない
}
}

const product = new Product(1, 'Laptop', 1000);

console.log(product.id); // 1
console.log(product.name); // 'Laptop'
console.log(product.price); // 1000

// OK: 通常のプロパティは変更可能
product.price = 1200;

// Error: readonlyプロパティは変更不可
// product.id = 2;
// product.name = 'Desktop';

アクセス修飾子の比較

修飾子クラス内継承先クラス外部
publicOKOKOK
protectedOKOKNG
privateOKNGNG
readonly初期化時のみ読み取りのみ読み取りのみ

コンストラクタの短縮記法

TypeScriptでは、コンストラクタの引数にアクセス修飾子を付けることで、プロパティの宣言と初期化を同時に行えます。

従来の書き方

class User {
public name: string;
private age: number;
protected email: string;

constructor(name: string, age: number, email: string) {
this.name = name;
this.age = age;
this.email = email;
}
}

短縮記法

class User {
// コンストラクタの引数にアクセス修飾子を付けるだけでOK
constructor(
public name: string,
private age: number,
protected email: string
) {
// プロパティの宣言と初期化が自動で行われる
}
}

const user = new User('Alice', 25, 'alice@example.com');
console.log(user.name); // 'Alice'

短縮記法の動作:

// この1行で以下の3つが自動的に行われる
constructor(public name: string) {}

// 上記は以下と同じ意味:
// 1. プロパティの宣言: public name: string;
// 2. パラメータの受け取り: constructor(name: string)
// 3. プロパティの初期化: this.name = name;

実践的な例

class Product {
constructor(
public readonly id: number,
public name: string,
private _price: number,
public category: string = 'general' // デフォルト値も設定可能
) {}

get price(): number {
return this._price;
}

set price(value: number) {
if (value > 0) {
this._price = value;
} else {
throw new Error('Price must be positive');
}
}
}

const product = new Product(1, 'Laptop', 1000);
console.log(product.id); // 1
console.log(product.name); // 'Laptop'
console.log(product.price); // 1000
console.log(product.category); // 'general'

ゲッターとセッター

ゲッターとセッターは、プロパティのように見えて実際にはメソッドとして動作する特殊な機能です。

基本的な使い方

class Person {
private _age: number = 0;

// ゲッター: プロパティのように読み取れる
get age(): number {
return this._age;
}

// セッター: プロパティのように値を設定できる
set age(value: number) {
if (value >= 0 && value <= 150) {
this._age = value;
} else {
throw new Error('Invalid age');
}
}
}

const person = new Person();

// ゲッターが呼ばれる(メソッドだが、()不要)
console.log(person.age); // 0

// セッターが呼ばれる
person.age = 25;
console.log(person.age); // 25

// セッターのバリデーションが働く
try {
person.age = -5; // エラーが投げられる
} catch (error) {
console.log((error as Error).message); // 'Invalid age'
}

ゲッター/セッターの特徴:

  • プロパティのようにアクセスできる(()不要)
  • 値の取得・設定時に処理を挟める
  • バリデーション、計算、フォーマットなどに使える

読み取り専用プロパティ(getterのみ)

class Rectangle {
constructor(
private width: number,
private height: number
) {}

// getterのみ(setterなし)= 読み取り専用
get area(): number {
return this.width * this.height;
}

get perimeter(): number {
return 2 * (this.width + this.height);
}
}

const rect = new Rectangle(10, 5);

console.log(rect.area); // 50
console.log(rect.perimeter); // 30

// areaはgetterのみなので設定できない
// rect.area = 100; // Error: Cannot assign to 'area' because it is a read-only property

実践的な例: 温度変換

class Temperature {
private _celsius: number = 0;

// 摂氏の取得・設定
get celsius(): number {
return this._celsius;
}

set celsius(value: number) {
this._celsius = value;
}

// 華氏の取得・設定(自動計算)
get fahrenheit(): number {
return (this._celsius * 9/5) + 32;
}

set fahrenheit(value: number) {
this._celsius = (value - 32) * 5/9;
}

// ケルビンの取得(読み取り専用)
get kelvin(): number {
return this._celsius + 273.15;
}
}

const temp = new Temperature();

// 摂氏で設定
temp.celsius = 100;
console.log(temp.celsius); // 100
console.log(temp.fahrenheit); // 212
console.log(temp.kelvin); // 373.15

// 華氏で設定(内部的には摂氏に変換される)
temp.fahrenheit = 32;
console.log(temp.celsius); // 0
console.log(temp.fahrenheit); // 32
console.log(temp.kelvin); // 273.15

accessor キーワード(TS 4.9+)

TypeScript 4.9 で導入された auto-accessor は、ゲッター/セッターのボイラープレートを削減する機能です。accessor キーワードを付けたプロパティは、自動的にプライベートな backing field とゲッター/セッターが生成されます。

class Person {
// accessor キーワードでゲッター/セッターを自動生成
accessor name: string;

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

// 上記は以下と等価
class PersonExpanded {
#name: string;

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

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

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

accessor の主な用途は decorator と組み合わせて値の get/set に処理を挿入することです(decorator は 10 章で扱います)。シンプルなゲッター/セッターが必要な場合は、従来通り public フィールドでも十分です。

定数とコンストラクタの組み合わせ(private readonly

コンストラクタパラメータプロパティ(短縮記法)と readonly を組み合わせると、外部から変更不可の private フィールドを最短で宣言できます。DI(依存性注入)やイミュータブルな value object を作るときに頻出するパターンです。

class UserService {
// private readonly の組み合わせ: 初期化後は変更不可、外部からも見えない
constructor(
private readonly db: Database,
private readonly logger: Logger,
) {}

async getUser(id: number): Promise<User | null> {
this.logger.info(`Fetching user ${id}`);
return this.db.users.findById(id);
}
}

// 呼び出し側
const service = new UserService(db, logger);
// service.db = otherDb; // Error: db は readonly

readonly は初期化後の再代入を禁止するだけで、ディープな不変性は保証しない点に注意してください。配列やオブジェクトの内部は変更可能です(完全に不変化したい場合は Readonly<T> / as const を組み合わせます)。

試してみよう: Userクラスを作ろう ★★

以下の要件を満たすUserクラスを作成してください。

要件:

  1. プロパティ:

    • id (readonly, number): ユーザーID
    • name (public, string): ユーザー名
    • email (private, string): メールアドレス
    • _password (private, string): パスワード(ハッシュ化済みと仮定)
    • createdAt (readonly, Date): 作成日時
  2. コンストラクタ: id, name, email, passwordを受け取る(createdAtは自動設定)

  3. メソッド:

    • getEmail(): メールアドレスを返す
    • updateEmail(newEmail): メールアドレスを更新する
    • checkPassword(input): パスワードが一致するか確認
    • getAccountAge(): アカウント作成からの日数を返す
  4. ゲッター/セッター:

    • displayName: nameを「さん」付きで返す
    • password: 設定のみ可能(取得は不可)
ヒント
  1. コンストラクタの短縮記法を使うと簡潔に書ける
  2. createdAtはコンストラクタ内でnew Date()を使って初期化
  3. パスワードのセッターでは、実際にはハッシュ化処理を行う(この演習では省略)
  4. アカウント日数はDate.now() - this.createdAt.getTime()で計算
回答と解説
class User {
// コンストラクタの短縮記法を使用
public readonly createdAt: Date;

constructor(
public readonly id: number,
public name: string,
private email: string,
private _password: string
) {
// createdAtは引数として受け取らず、自動で現在時刻を設定
this.createdAt = new Date();
}

// メールアドレスを取得
getEmail(): string {
return this.email;
}

// メールアドレスを更新
updateEmail(newEmail: string): void {
// 簡易的なバリデーション
if (!newEmail.includes('@')) {
throw new Error('Invalid email format');
}
this.email = newEmail;
console.log('Email updated successfully');
}

// パスワードが一致するか確認
checkPassword(input: string): boolean {
// 実際にはハッシュ化して比較する
return this._password === input;
}

// アカウント作成からの日数を返す
getAccountAge(): number {
const now = Date.now();
const created = this.createdAt.getTime();
const diffMs = now - created;
// ミリ秒を日数に変換
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
}

// 「さん」付きの名前を返すゲッター
get displayName(): string {
return `${this.name}さん`;
}

// パスワードは設定のみ可能(取得は不可)
set password(newPassword: string) {
// 実際にはここでハッシュ化処理を行う
if (newPassword.length < 8) {
throw new Error('Password must be at least 8 characters');
}
this._password = newPassword;
console.log('Password updated successfully');
}
}

// テスト
const user = new User(1, 'Alice', 'alice@example.com', 'password123');

// プロパティのアクセス
console.log(user.id); // 1
console.log(user.name); // 'Alice'
console.log(user.displayName); // 'Aliceさん'
console.log(user.createdAt); // 現在の日時

// メソッドの呼び出し
console.log(user.getEmail()); // 'alice@example.com'
console.log(user.checkPassword('password123')); // true
console.log(user.checkPassword('wrongpass')); // false
console.log(user.getAccountAge()); // 0(作成直後)

// メールアドレスの更新
user.updateEmail('alice.new@example.com');
console.log(user.getEmail()); // 'alice.new@example.com'

// パスワードの更新(セッター経由)
user.password = 'newpassword123';
console.log(user.checkPassword('newpassword123')); // true

// エラーケース
try {
user.password = 'short'; // 8文字未満はエラー
} catch (error) {
console.log((error as Error).message); // 'Password must be at least 8 characters'
}

try {
user.updateEmail('invalid-email'); // @がないとエラー
} catch (error) {
console.log((error as Error).message); // 'Invalid email format'
}

解説:

  • readonlyでidとcreatedAtを不変に
  • privateでemailと_passwordを外部から隠蔽
  • ゲッターdisplayNameで加工した値を返す
  • セッターpasswordでバリデーションを挟む
  • メソッドでデータの取得・更新をカプセル化

まとめ

この章で学んだこと:

  • クラスの定義: プロパティ、コンストラクタ、メソッドを持つオブジェクトの設計図
  • コンストラクタ: インスタンス生成時に呼ばれる初期化メソッド
  • アクセス修飾子: public(公開)、private(非公開)、protected(継承先まで公開)、readonly(読み取り専用)
  • コンストラクタの短縮記法: 引数にアクセス修飾子を付けて宣言と初期化を同時に行う
  • ゲッターとセッター: プロパティのようにアクセスできるメソッド

次の章では、クラスの継承とインターフェースについて学びます。オブジェクト指向プログラミングのより高度な概念を扱います。

初学者がつまずきやすいポイント

警告

よくある間違い

❌ プロパティの初期化を忘れる

// ❌ 間違い: プロパティが初期化されていない
class User {
name: string; // Error: Property 'name' has no initializer
age: number;
}

// ✅ 正しい方法1: コンストラクタで初期化
class User {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}

// ✅ 正しい方法2: 初期値を設定
class User {
name: string = '';
age: number = 0;
}

// ✅ 正しい方法3: コンストラクタの短縮記法
class User {
constructor(public name: string, public age: number) {}
}

原因: strictPropertyInitializationが有効だと、プロパティは宣言時またはコンストラクタで初期化が必要です。

解決策: 初期値の設定、コンストラクタでの初期化、または短縮記法を使用してください。

❌ privateとreadonlyを混同する

class Config {
private apiKey: string; // 外部からアクセス不可、内部では変更可能
readonly version: string; // 外部からアクセス可能、変更不可

constructor(apiKey: string, version: string) {
this.apiKey = apiKey;
this.version = version;
}

rotateKey(newKey: string) {
this.apiKey = newKey; // OK: privateは内部で変更可能
// this.version = '2.0'; // Error: readonlyは変更不可
}
}

const config = new Config('secret', '1.0');
// console.log(config.apiKey); // Error: privateはアクセス不可
console.log(config.version); // OK: readonlyは読み取り可能

原因: privateはアクセス制限、readonlyは変更制限という異なる目的があります。

解決策: 外部からのアクセスを禁止する場合はprivate、変更を禁止する場合はreadonlyを使用してください。

❌ コンストラクタの短縮記法でアクセス修飾子を忘れる

// ❌ 間違い: アクセス修飾子がないとプロパティが作成されない
class User {
constructor(name: string, age: number) {
// this.name = name; // Error: 'name'プロパティは存在しない
}
}

// ✅ 正しい: アクセス修飾子を付けるとプロパティが作成される
class User {
constructor(public name: string, public age: number) {
// プロパティの宣言と初期化が自動的に行われる
}
}

原因: 短縮記法はアクセス修飾子(public, private, protected, readonly)がある場合のみ機能します。

解決策: 短縮記法を使う場合は必ずアクセス修飾子を付けてください。

❌ ゲッターのみでセッターも呼べると思う

class Rectangle {
constructor(private _width: number, private _height: number) {}

// ゲッターのみ
get area(): number {
return this._width * this._height;
}
}

const rect = new Rectangle(10, 5);
console.log(rect.area); // 50

// ❌ 間違い: セッターがないプロパティに代入しようとする
rect.area = 100; // Error: Cannot assign to 'area' because it is a read-only property

原因: ゲッターのみでセッターがないプロパティは読み取り専用になります。

解決策: 変更可能にする場合はセッターも定義してください。計算値は通常セッターを持たせません。

❌ thisを忘れてプロパティにアクセス

class Counter {
count: number = 0;

// ❌ 間違い: thisを忘れている
incrementWrong() {
count++; // Error: 'count'という名前は存在しない
}

// ✅ 正しい: thisを使ってプロパティにアクセス
incrementCorrect() {
this.count++;
}
}

原因: クラス内でプロパティにアクセスするにはthisが必要です。

解決策: クラス内でプロパティやメソッドにアクセスする際は常にthis.を使用してください。


次に読む