TypeScript – 類別(Classes)
class 宣告與成員
簡介
在 JavaScript 進入 ES6 後,class 成為語言的原生語法,讓物件導向(OOP)程式設計變得更直觀。TypeScript 進一步在此基礎上加入 型別系統、修飾子(modifier)、抽象類別 等功能,使得大型專案的結構更清晰、錯誤更容易在編譯階段被捕捉。
本單元將說明 class 的宣告方式、成員的種類與存取控制,並透過實務範例展示如何在 TypeScript 中寫出安全、可維護的類別。無論是剛接觸 JavaScript 的新手,或是已有 JavaScript 背景想升級到 TypeScript 的開發者,都能從這篇文章獲得實用的知識。
核心概念
1. 基本的 class 宣告
在 TypeScript 中,class 的語法與 ES6 幾乎相同,只是可以在屬性與方法上直接加上型別註記。
class Person {
// 成員變數(屬性)必須先宣告型別
name: string;
age: number;
// 建構子(constructor)負責初始化屬性
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// 方法(member function)
greet(): void {
console.log(`Hello, I'm ${this.name}, ${this.age} years old.`);
}
}
重點:在 TypeScript 中,屬性若未指定存取修飾子,預設為
public,即外部可以直接讀寫。
2. 存取修飾子(public、private、protected、readonly)
| 修飾子 | 可見範圍 | 典型用途 |
|---|---|---|
public |
任意位置(預設) | API 介面、外部可直接使用的屬性 |
private |
只在 class 本身內部可見 | 隱藏實作細節,避免外部誤用 |
protected |
本身與子類別可見 | 供繼承使用的共用屬性或方法 |
readonly |
只能在宣告或建構子中賦值 | 常數屬性、不可變資料 |
class BankAccount {
// 只能在建構子或宣告時賦值,之後不可變
readonly accountNumber: string;
// 僅在本類別內部使用,外部無法直接存取
private balance: number = 0;
constructor(accountNumber: string) {
this.accountNumber = accountNumber;
}
// 公開的存款方法,內部會修改 private 成員
deposit(amount: number): void {
if (amount <= 0) throw new Error('Amount must be positive');
this.balance += amount;
}
// 只給予讀取餘額的權限,避免外部直接改寫
getBalance(): number {
return this.balance;
}
}
3. 參數屬性(Parameter Properties)
TypeScript 允許在建構子參數前直接加上修飾子,省去先宣告再賦值的步驟。
class Point {
// 直接在參數前加上 public,會自動產生對應的屬性
constructor(public x: number, public y: number) {}
}
// 使用
const p = new Point(10, 20);
console.log(p.x, p.y); // 10 20
技巧:若屬性僅在建構子內使用且不需要外部存取,可使用
private或protected,同時保持程式碼簡潔。
4. 靜態成員(static)
static 成員屬於類別本身,而非實例。常用於 常數、工具方法 或 共享狀態。
class MathUtil {
// 靜態常數
static readonly PI = 3.14159;
// 靜態方法
static areaOfCircle(radius: number): number {
return MathUtil.PI * radius * radius;
}
}
// 呼叫方式不需要建立實例
console.log(MathUtil.areaOfCircle(5)); // 78.53975
5. 抽象類別與抽象方法(abstract)
抽象類別無法直接實例化,只能被繼承。抽象方法則必須在子類別中實作。
abstract class Animal {
// 抽象方法,子類別必須實作
abstract makeSound(): void;
// 具體方法,所有子類別皆可共用
move(): void {
console.log('Moving...');
}
}
class Dog extends Animal {
makeSound(): void {
console.log('Woof!');
}
}
const d = new Dog();
d.makeSound(); // Woof!
d.move(); // Moving...
6. 介面(interface)與類別的結合
TypeScript 允許類別 實作(implements)介面,以保證類別具備介面所定義的結構。
interface ILogger {
log(message: string): void;
error(message: string): void;
}
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[INFO] ${message}`);
}
error(message: string): void {
console.error(`[ERROR] ${message}`);
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
| 忘記加修飾子 | 屬性預設為 public,不小心把本應私有的資料暴露。 |
主動在屬性前寫上 private 或 protected,讓意圖更明確。 |
在建構子外直接改寫 readonly |
TypeScript 允許在任何地方改寫 readonly(除非使用 --noImplicitOverride)。 |
使用 readonly 搭配 private,或在 tsconfig 開啟 exactOptionalPropertyTypes 以加強檢查。 |
| 靜態與實例屬性混淆 | 用 this.xxx 讀取 static 成員會出錯。 |
使用類別名稱(如 ClassName.xxx)存取靜態成員。 |
| 抽象類別未實作所有抽象方法 | 編譯器會報錯,但若使用 any 逃過檢查,會在執行時出現 undefined。 |
嚴格模式(strict:true)下,編譯器會強制檢查。 |
過度使用 any |
失去 TypeScript 型別保護的優勢。 | 盡量使用具體型別,或在不確定時使用 unknown 再做型別斷言。 |
其他最佳實踐
- 使用
private/protected隱藏實作細節,只暴露必要的 API。 - 依功能分層:將純資料結構放在 DTO(Data Transfer Object)類別,將業務邏輯放在 Service 類別。
- 遵循 SOLID 原則,尤其是 單一職責原則(SRP),避免類別過於龐大。
- 配合 Lint(如 ESLint + @typescript-eslint),自動檢測未使用的
public成員或不必要的any。 - 使用
readonly讓不可變的屬性在編譯階段即被保護,減少 bug。
實際應用場景
1. 前端 UI 元件(React + TypeScript)
在大型 React 專案中,使用 class 來封裝 UI 元件的狀態與行為,配合 interface 定義 props,能讓組件的 API 更清晰。
import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
class Button extends React.Component<ButtonProps> {
// 使用 readonly 表示 props 不會在組件內被改寫
readonly props: ButtonProps;
render() {
const { label, onClick, disabled } = this.props;
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
}
2. 後端服務(Node.js + TypeScript)
在建立 RESTful API 時,以 class 表示資料模型與商業邏輯,搭配 private 資料庫連線,確保外部無法直接存取。
import { Pool } from 'pg';
class UserRepository {
private readonly pool: Pool;
constructor(pool: Pool) {
this.pool = pool;
}
async findById(id: number) {
const res = await this.pool.query('SELECT * FROM users WHERE id = $1', [id]);
return res.rows[0];
}
// 其他 CRUD 方法...
}
3. Game 開發(Entity Component System)
在遊戲開發中,每個遊戲實體(Entity)可用 class 表示,並透過繼承或組合方式擴充功能。
abstract class GameObject {
constructor(public x: number, public y: number) {}
abstract update(delta: number): void;
}
class Player extends GameObject {
private speed: number = 5;
update(delta: number): void {
// 依據使用者輸入更新位置
this.x += this.speed * delta;
}
}
總結
- TypeScript 的
class在保留 JavaScript 原生語法的同時,提供 型別、修飾子、抽象 等強大功能,使得物件導向程式設計更安全、可維護。 - 透過
public/private/protected/readonly以及static、抽象類別,開發者可以清楚劃分 API 與實作細節。 - 參數屬性、介面實作、抽象方法 等語法讓類別的宣告更加簡潔且具表意性。
- 在實務上,無論是前端 UI、後端服務或是遊戲開發,正確運用 class 能提升程式碼可讀性、降低錯誤率,並且與 TypeScript 的型別系統相輔相成,讓大型專案更易於管理與擴充。
掌握了 class 宣告與成員 的核心概念後,接下來可以進一步探索 繼承、介面合併(mixins) 以及 Decorator 等進階特性,讓你的 TypeScript 程式碼更具彈性與表現力。祝開發順利!