TypeScript 類別 (Class)

TypeScript 完整支援 ES6 的類別語法,並在此基礎上加入了型別標註和存取修飾子等功能,讓物件導向程式設計更加完善。

基本類別定義

class Person {
  name: string;
  age: number;

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

  greet(): string {
    return `你好,我是 ${this.name},今年 ${this.age} 歲`;
  }
}

const person = new Person('小明', 25);
console.log(person.greet()); // 你好,我是小明,今年 25 歲

存取修飾子 (Access Modifiers)

TypeScript 提供三種存取修飾子:

public (公開)

預設值,可以從任何地方存取:

class Animal {
  public name: string;

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

  public speak(): void {
    console.log(`${this.name} makes a sound`);
  }
}

private (私有)

只能在類別內部存取:

class BankAccount {
  private balance: number;

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

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

  withdraw(amount: number): boolean {
    if (amount <= this.balance) {
      this.balance -= amount;
      return true;
    }
    return false;
  }

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

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// account.balance;  // 錯誤:'balance' 是私有屬性

protected (受保護)

只能在類別內部和子類別中存取:

class Animal {
  protected name: string;

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

class Dog extends Animal {
  bark(): void {
    console.log(`${this.name} says: 汪汪!`); // OK,可以存取 protected
  }
}

const dog = new Dog('小黑');
dog.bark(); // 小黑 says: 汪汪!
// dog.name;   // 錯誤:'name' 是受保護屬性

參數屬性 (Parameter Properties)

TypeScript 提供簡化語法,在建構子參數前加上修飾子:

// 傳統寫法
class Person {
  private name: string;
  private age: number;

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

// 簡化寫法
class Person {
  constructor(
    private name: string,
    private age: number
  ) {}

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

唯讀屬性 (readonly)

class Point {
  readonly x: number;
  readonly y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

const point = new Point(10, 20);
// point.x = 30;  // 錯誤:無法指派給 'x',因為它是唯讀屬性

結合參數屬性:

class Point {
  constructor(
    readonly x: number,
    readonly y: number
  ) {}
}

Getter 和 Setter

class Circle {
  private _radius: number;

  constructor(radius: number) {
    this._radius = radius;
  }

  get radius(): number {
    return this._radius;
  }

  set radius(value: number) {
    if (value <= 0) {
      throw new Error('Radius must be positive');
    }
    this._radius = value;
  }

  get area(): number {
    return Math.PI * this._radius ** 2;
  }
}

const circle = new Circle(5);
console.log(circle.radius); // 5
console.log(circle.area); // 78.54...
circle.radius = 10;
console.log(circle.area); // 314.16...

靜態成員 (Static Members)

靜態成員屬於類別本身,而非實例:

class MathUtils {
  static PI = 3.14159;

  static add(a: number, b: number): number {
    return a + b;
  }

  static multiply(a: number, b: number): number {
    return a * b;
  }
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(2, 3)); // 5

// 不能透過實例存取
// const utils = new MathUtils();
// utils.PI;  // 錯誤

靜態區塊 (Static Blocks)

TypeScript 4.4+ 支援靜態初始化區塊:

class Database {
  static connection: any;

  static {
    // 靜態初始化程式碼
    this.connection = createConnection();
    console.log('Database initialized');
  }
}

類別繼承 (Inheritance)

使用 extends 繼承類別:

class Animal {
  constructor(protected name: string) {}

  speak(): void {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(
    name: string,
    private breed: string
  ) {
    super(name); // 呼叫父類別建構子
  }

  speak(): void {
    console.log(`${this.name} barks!`);
  }

  getBreed(): string {
    return this.breed;
  }
}

class Cat extends Animal {
  speak(): void {
    console.log(`${this.name} meows!`);
  }
}

const dog = new Dog('小黑', '柴犬');
dog.speak(); // 小黑 barks!

const cat = new Cat('小花');
cat.speak(); // 小花 meows!

實作介面 (implements)

interface Printable {
  print(): void;
}

interface Savable {
  save(): void;
}

class Document implements Printable, Savable {
  constructor(private content: string) {}

  print(): void {
    console.log(`Printing: ${this.content}`);
  }

  save(): void {
    console.log(`Saving: ${this.content}`);
  }
}

泛型類別

class Container<T> {
  private items: T[] = [];

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

  get(index: number): T | undefined {
    return this.items[index];
  }

  getAll(): T[] {
    return [...this.items];
  }
}

const numberContainer = new Container<number>();
numberContainer.add(1);
numberContainer.add(2);

const stringContainer = new Container<string>();
stringContainer.add('hello');
stringContainer.add('world');

this 型別

class Builder {
  private value: number = 0;

  add(n: number): this {
    this.value += n;
    return this;
  }

  multiply(n: number): this {
    this.value *= n;
    return this;
  }

  getValue(): number {
    return this.value;
  }
}

// 鏈式呼叫
const result = new Builder().add(5).multiply(2).add(3).getValue(); // 13

類別表達式

const Rectangle = class {
  constructor(
    public width: number,
    public height: number
  ) {}

  getArea(): number {
    return this.width * this.height;
  }
};

const rect = new Rectangle(10, 20);
console.log(rect.getArea()); // 200

override 關鍵字

TypeScript 4.3+ 提供 override 關鍵字確保正確覆寫父類別方法:

class Animal {
  speak(): void {
    console.log('Animal speaks');
  }
}

class Dog extends Animal {
  override speak(): void {
    console.log('Dog barks');
  }

  // override invalidMethod(): void {}  // 錯誤:父類別沒有這個方法
}

需要在 tsconfig.json 中啟用 noImplicitOverride

{
  "compilerOptions": {
    "noImplicitOverride": true
  }
}

實用範例

單例模式 (Singleton)

class Singleton {
  private static instance: Singleton;

  private constructor() {}

  static getInstance(): Singleton {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true

工廠模式

abstract class Shape {
  abstract getArea(): number;
}

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

  getArea(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle extends Shape {
  constructor(
    private width: number,
    private height: number
  ) {
    super();
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class ShapeFactory {
  static createCircle(radius: number): Circle {
    return new Circle(radius);
  }

  static createRectangle(width: number, height: number): Rectangle {
    return new Rectangle(width, height);
  }
}

const circle = ShapeFactory.createCircle(5);
const rectangle = ShapeFactory.createRectangle(4, 6);

觀察者模式

interface Observer {
  update(data: any): void;
}

class Subject {
  private observers: Observer[] = [];

  subscribe(observer: Observer): void {
    this.observers.push(observer);
  }

  unsubscribe(observer: Observer): void {
    this.observers = this.observers.filter((o) => o !== observer);
  }

  notify(data: any): void {
    this.observers.forEach((observer) => observer.update(data));
  }
}

class ConcreteObserver implements Observer {
  constructor(private name: string) {}

  update(data: any): void {
    console.log(`${this.name} received: ${data}`);
  }
}

const subject = new Subject();
const observer1 = new ConcreteObserver('Observer 1');
const observer2 = new ConcreteObserver('Observer 2');

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Hello!');
// Observer 1 received: Hello!
// Observer 2 received: Hello!