TypeScript 程式語言教學手冊
適用對象:新進前端開發同仁
目標:快速掌握 TypeScript 在專案中的使用方法與最佳實踐
更新日期:2026年2月20日
📋 目錄
- TypeScript 基礎
- 1.1 TypeScript 與 JavaScript 的差異
- 1.2 型別系統
- 1.3 介面與型別別名
- 專案規範
- 2.1 TypeScript 版本與設定
- 2.2 檔案命名規則與專案目錄結構
- 2.3 型別宣告的最佳實踐
- 2.4 使用 any 的限制與替代方法
- 程式開發指引
- 3.1 撰寫可讀性與可維護性的程式碼
- 3.2 常見錯誤與最佳解法
- 3.3 與第三方套件整合
- 範例程式碼
- 4.1 Class 類別實作範例
- 4.2 Interface 介面範例
- 4.3 泛型實作範例
- 4.4 模組化範例
- 實務建議
- 5.1 善用 TypeScript 的型別檢查
- 5.2 除錯與測試
- 5.3 團隊協作與程式風格統一
- 檢查清單
- 進階主題
- 效能優化與最佳實踐
- TypeScript 生態系統
- 常見問題與解決方案
- TypeScript 6.0 與 7.0 新特性與遷移
1. TypeScript 基礎
1.1 TypeScript 與 JavaScript 的差異
什麼是 TypeScript?
TypeScript 是 Microsoft 開發的 JavaScript 超集(superset),為 JavaScript 添加了靜態型別檢查功能。
主要差異比較
| 特性 | JavaScript | TypeScript |
|---|---|---|
| 型別檢查 | 動態型別(執行時期) | 靜態型別(編譯時期) |
| 語法 | ES2015+ | ES2015+ + 型別註解 |
| 編譯 | 直接執行 | 需編譯為 JavaScript |
| 錯誤偵測 | 執行時期 | 開發時期 |
| IDE 支援 | 基本 | 強大的自動完成與錯誤提示 |
| 最低編譯目標 | N/A | ES2015(TS 6.0 起已棄用 ES5) |
為什麼使用 TypeScript?
優點:
- 🔍 編譯時期錯誤檢查:在開發階段就能發現錯誤
- 🎯 更好的 IDE 支援:自動完成、重構、導航
- 📚 自我文件化:型別即文件,提升程式可讀性
- 🛡️ 重構安全性:大型專案重構時更安全
- 👥 團隊協作:明確的介面定義,降低溝通成本
範例比較:
// JavaScript - 執行時期才發現錯誤
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// 傳入錯誤的資料結構,執行時期才會報錯
calculateTotal([{ name: "商品", cost: 100 }]); // Error: Cannot read property 'price'// TypeScript - 開發時期就能發現錯誤
interface Item {
name: string;
price: number;
}
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// 編譯時期就會提示錯誤
calculateTotal([{ name: "商品", cost: 100 }]); // 編譯錯誤:缺少 'price' 屬性1.2 型別系統
基本型別
TypeScript 提供了多種基本型別來描述資料:
// 基本型別
let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let list: number[] = [1, 2, 3];
let tuple: [string, number] = ["hello", 10];
// null 和 undefined
let u: undefined = undefined;
let n: null = null;
// void - 通常用於沒有回傳值的函數
function warnUser(): void {
console.log("This is my warning message");
}
// never - 永遠不會回傳的函數
function error(message: string): never {
throw new Error(message);
}物件型別
// 物件型別定義
let user: {
name: string;
age: number;
email?: string; // 可選屬性
} = {
name: "王小明",
age: 25
};
// 使用 interface 定義物件型別(推薦)
interface User {
readonly id: number; // 唯讀屬性
name: string;
age: number;
email?: string; // 可選屬性
[propName: string]: any; // 索引簽名,允許額外屬性
}
const userData: User = {
id: 1,
name: "李小華",
age: 30,
email: "lee@example.com",
department: "IT" // 額外屬性
};陣列型別
// 陣列型別的兩種寫法
let numbers1: number[] = [1, 2, 3, 4, 5];
let numbers2: Array<number> = [1, 2, 3, 4, 5];
// 物件陣列
interface Product {
id: number;
name: string;
price: number;
}
let products: Product[] = [
{ id: 1, name: "筆電", price: 30000 },
{ id: 2, name: "滑鼠", price: 800 }
];
// 多維陣列
let matrix: number[][] = [[1, 2], [3, 4]];泛型 (Generics)
泛型讓我們能撰寫可重用的程式碼,同時保持型別安全:
// 基本泛型函數
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString");
let output2 = identity<number>(100);
// 泛型介面
interface GenericIdentityFn<T> {
(arg: T): T;
}
// 泛型類別
class GenericRepository<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
findById(id: number): T | undefined {
return this.items[id];
}
getAll(): T[] {
return this.items;
}
}
// 使用泛型類別
const userRepository = new GenericRepository<User>();
userRepository.add({ id: 1, name: "測試用戶", age: 25 });實務注意事項:
- 優先使用具體型別,只有在需要重用時才使用泛型
- 為泛型參數選擇有意義的名稱(如
TUser而非T) - 適當使用泛型約束來限制泛型型別
1.3 介面 (Interface) 與型別別名 (Type Alias)
Interface 介面
介面定義物件的結構,是 TypeScript 中定義型別的主要方式:
// 基本介面定義
interface Person {
name: string;
age: number;
}
// 介面繼承
interface Employee extends Person {
employeeId: string;
department: string;
salary?: number; // 可選屬性
}
// 實作介面
class Developer implements Employee {
name: string;
age: number;
employeeId: string;
department: string;
constructor(name: string, age: number, employeeId: string) {
this.name = name;
this.age = age;
this.employeeId = employeeId;
this.department = "開發部";
}
}
// 函數介面
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function(source: string, subString: string): boolean {
return source.search(subString) > -1;
};Type Alias 型別別名
型別別名為現有型別建立新名稱:
// 基本型別別名
type ID = string | number;
type UserStatus = "active" | "inactive" | "pending";
// 物件型別別名
type Point = {
x: number;
y: number;
};
// 函數型別別名
type EventHandler = (event: Event) => void;
// 泛型型別別名
type ApiResponse<T> = {
data: T;
success: boolean;
message: string;
};
// 使用範例
type UserResponse = ApiResponse<User>;Interface vs Type Alias 選擇指南
| 使用情境 | 推薦 | 原因 |
|---|---|---|
| 定義物件結構 | Interface | 更清晰的語法,支援聲明合併 |
| 定義函數型別 | Type Alias | 更簡潔 |
| Union/Intersection 型別 | Type Alias | Interface 不支援 |
| 需要繼承 | Interface | 語法更直觀 |
| 計算屬性名 | Type Alias | Interface 不支援 |
// 推薦:使用 Interface 定義物件
interface UserConfig {
apiUrl: string;
timeout: number;
}
// 推薦:使用 Type 定義 Union 型別
type Theme = "light" | "dark" | "auto";
// 推薦:使用 Type 定義複雜型別
type EventMap = {
click: MouseEvent;
keypress: KeyboardEvent;
focus: FocusEvent;
};本章重點回顧:
- TypeScript 提供編譯時期的型別檢查,大幅提升開發效率
- 掌握基本型別系統是使用 TypeScript 的基礎
- Interface 適合定義物件結構,Type Alias 適合複雜型別操作
- 泛型讓程式碼更具重用性,但要適度使用
下一章預告: 我們將學習專案中的 TypeScript 配置與開發規範。
2. 專案規範
2.1 TypeScript 版本與設定
建議的 TypeScript 版本
{
"devDependencies": {
"typescript": "^5.9.0",
"@types/node": "^22.0.0"
}
}重要提示: TypeScript 6.0 已進入 Beta 階段(截至 2026年2月),而 TypeScript 7.0 將是基於 Go 語言的全新原生編譯器,帶來預計 10 倍的編譯速度提升。TypeScript 6.0 是最後一個基於 JavaScript 的版本,作為 5.9 到 7.0 的過渡橋樑。詳見 11. TypeScript 6.0 與 7.0 新特性與遷移。
版本選擇原則:
- 使用最新穩定版本(目前為 5.9)確保穩定性
- 可評估採用 TypeScript 6.0 Beta 以提前準備遷移
- 團隊統一使用相同的 TypeScript 版本
- 關注 TypeScript 7.0 原生預覽版 (
@typescript/native-preview) 進展
tsconfig.json 專案配置
專案中的 tsconfig.json 是 TypeScript 編譯器的配置檔案:
{
"compilerOptions": {
// 編譯目標(TS 6.0 預設為 es2025)
"target": "ES2025",
"module": "ESNext",
"moduleResolution": "bundler",
// 輸出設定
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// 型別檢查設定(TS 6.0 預設 strict: true)
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noUncheckedSideEffectImports": true,
// 模組解析(TS 6.0 已棄用 baseUrl,改用 paths 直接指定完整路徑)
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/utils/*": ["./src/utils/*"],
"@/types/*": ["./src/types/*"]
},
// 明確指定需要的 @types 套件(TS 6.0 預設 types: [])
"types": ["node"],
// 其他設定(TS 6.0 esModuleInterop 始終啟用)
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": [
"src/**/*",
"types/**/*"
],
"exclude": [
"node_modules",
"dist",
"build",
"**/*.test.ts",
"**/*.spec.ts"
]
}TypeScript 6.0 重大預設值變更:
strict預設為truetarget預設為最新 ES 標準版本(目前為es2025)module預設為esnexttypes預設為[](需明確列出所需的@types套件)rootDir預設為.(tsconfig.json所在目錄)baseUrl已棄用,請直接在paths中使用完整相對路徑moduleResolution: node(node10)已棄用,請改用nodenext或bundleresModuleInterop和allowSyntheticDefaultImports始終啟用,無法設為false
重要配置說明
| 配置項目 | 說明 | 建議值 | TS 6.0 預設 |
|---|---|---|---|
strict | 啟用所有嚴格型別檢查 | true | true |
noImplicitAny | 禁止隱式 any 型別 | true | true(含在 strict 中) |
strictNullChecks | 嚴格檢查 null/undefined | true | true(含在 strict 中) |
noUnusedLocals | 檢查未使用的變數 | true | false |
noUnusedParameters | 檢查未使用的參數 | true | false |
exactOptionalPropertyTypes | 精確檢查可選屬性 | true | false |
noUncheckedSideEffectImports | 檢查副作用匯入拼寫 | true | true |
types | 指定全域型別套件 | ["node"] | [] |
target | 編譯目標 | ES2025 | es2025 |
moduleResolution | 模組解析策略 | bundler 或 nodenext | 依 module 推斷 |
2.2 檔案命名規則與專案目錄結構
檔案命名規則
專案根目錄/
├── src/
│ ├── components/ # React/Vue 元件
│ │ ├── common/ # 共用元件
│ │ │ ├── Button.tsx
│ │ │ ├── Modal.tsx
│ │ │ └── index.ts # 統一匯出
│ │ └── pages/ # 頁面元件
│ │ ├── HomePage.tsx
│ │ └── UserProfile.tsx
│ ├── hooks/ # 自定義 hooks (React)
│ │ ├── useAuth.ts
│ │ └── useLocalStorage.ts
│ ├── services/ # API 服務層
│ │ ├── api.ts
│ │ ├── authService.ts
│ │ └── userService.ts
│ ├── store/ # 狀態管理
│ │ ├── index.ts
│ │ ├── authStore.ts
│ │ └── userStore.ts
│ ├── types/ # 型別定義
│ │ ├── api.types.ts
│ │ ├── user.types.ts
│ │ └── common.types.ts
│ ├── utils/ # 工具函數
│ │ ├── helpers.ts
│ │ ├── constants.ts
│ │ └── formatters.ts
│ ├── styles/ # 樣式檔案
│ └── assets/ # 靜態資源
├── types/ # 全域型別定義
│ └── global.d.ts
├── tests/ # 測試檔案
└── docs/ # 說明文件命名慣例
檔案命名:
// ✅ 好的命名
UserProfile.tsx // 元件:PascalCase
userService.ts // 服務:camelCase
auth.types.ts // 型別定義:camelCase + .types
constants.ts // 常數:camelCase
api-client.ts // 連字符號可接受
// ❌ 避免的命名
user_profile.tsx // 避免底線
UserProfile.ts // 元件檔案使用 .tsx
AUTH_SERVICE.ts // 避免全大寫變數與函數命名:
// ✅ 好的命名
const userName = "王小明"; // camelCase
const API_BASE_URL = "https://api.example.com"; // 常數:UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
function getUserById(id: string): User { ... } // 函數:camelCase,動詞開頭
function calculateTotalPrice(items: Item[]): number { ... }
// ✅ 類別和介面命名
class UserManager { ... } // 類別:PascalCase
interface ApiResponse<T> { ... } // 介面:PascalCase
type UserStatus = "active" | "inactive"; // 型別:PascalCase2.3 型別宣告的最佳實踐
型別定義組織
// types/user.types.ts - 用戶相關型別
export interface User {
id: string;
name: string;
email: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
export type UserRole = "admin" | "user" | "guest";
export interface CreateUserRequest {
name: string;
email: string;
password: string;
}
export interface UpdateUserRequest extends Partial<Omit<User, "id" | "createdAt">> {}
// types/api.types.ts - API 相關型別
export interface ApiResponse<T = any> {
data: T;
success: boolean;
message: string;
errors?: string[];
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// types/common.types.ts - 通用型別
export type Status = "loading" | "success" | "error" | "idle";
export interface SelectOption<T = string> {
label: string;
value: T;
disabled?: boolean;
}型別導入與導出規範
// ✅ 明確的型別導入
import type { User, UserRole } from "@/types/user.types";
import type { ApiResponse } from "@/types/api.types";
// ✅ 命名空間組織(大型專案)
declare namespace API {
interface Response<T> {
data: T;
success: boolean;
}
namespace User {
interface Detail {
id: string;
name: string;
}
interface CreateRequest {
name: string;
email: string;
}
}
}
// 使用命名空間
function handleResponse(response: API.Response<API.User.Detail>) {
// ...
}2.4 使用 any 的限制與替代方法
any 的問題
// ❌ 問題:失去型別安全性
let data: any = fetchUserData();
data.nonExistentMethod(); // 編譯時不會報錯,執行時會出錯替代方案
1. 使用 unknown 型別
// ✅ 使用 unknown 替代 any
function processApiResponse(response: unknown): User | null {
// 需要型別守衛
if (isUser(response)) {
return response;
}
return null;
}
// 型別守衛函數
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
typeof (obj as User).id === "string" &&
typeof (obj as User).name === "string"
);
}2. 使用泛型
// ✅ 使用泛型保持型別安全
interface Repository<T> {
findById(id: string): Promise<T | null>;
create(entity: Omit<T, "id">): Promise<T>;
update(id: string, entity: Partial<T>): Promise<T>;
delete(id: string): Promise<boolean>;
}
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> {
// 實作...
}
// 其他方法實作...
}3. 使用 Union 型別
// ✅ 明確定義可能的型別
type ApiData = User | Product | Order | null;
function handleApiData(data: ApiData) {
if (!data) return;
// 使用 discriminated union
switch (data.type) {
case "user":
// TypeScript 知道這是 User 型別
console.log(data.name);
break;
case "product":
// TypeScript 知道這是 Product 型別
console.log(data.price);
break;
}
}4. 使用型別斷言(謹慎使用)
// ✅ 適當的型別斷言
interface LegacyApiResponse {
[key: string]: any;
}
function migrateLegacyData(legacy: LegacyApiResponse): User {
// 明確的型別轉換
return {
id: legacy.user_id as string,
name: legacy.user_name as string,
email: legacy.email_address as string,
role: (legacy.role as string) || "user"
} as User;
}any 使用準則
允許使用 any 的情況:
- 遷移現有 JavaScript 程式碼時的漸進式轉換
- 整合沒有型別定義的第三方函式庫
- 動態內容處理(如 JSON 解析)
使用 any 時的注意事項:
// ✅ 加上註解說明原因
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const legacyData: any = JSON.parse(legacyJsonString); // 舊系統資料格式未定義
// ✅ 儘快轉換為具體型別
const userData = validateAndTransformUser(legacyData);本章重點回顧:
- 正確配置
tsconfig.json是專案成功的基礎 - 統一的命名規則提升程式碼可讀性
- 良好的目錄結構有助於專案維護
- 避免使用
any,優先考慮unknown、泛型、Union 型別等替代方案
下一章預告: 我們將深入探討如何撰寫高品質的 TypeScript 程式碼。
3. 程式開發指引
3.1 撰寫可讀性與可維護性的程式碼
函數設計原則
單一職責原則
// ❌ 職責混雜的函數
function processUserData(userData: any): any {
// 驗證資料
if (!userData.email || !userData.name) {
throw new Error("資料不完整");
}
// 格式化資料
userData.email = userData.email.toLowerCase();
userData.name = userData.name.trim();
// 儲存到資料庫
database.save(userData);
// 發送歡迎信件
emailService.sendWelcomeEmail(userData.email);
return userData;
}
// ✅ 拆分為多個單一職責函數
interface UserInput {
name: string;
email: string;
}
interface ValidatedUser {
name: string;
email: string;
}
function validateUserInput(input: UserInput): ValidatedUser {
if (!input.email || !input.name) {
throw new Error("使用者姓名和電子信箱為必填欄位");
}
return {
name: input.name.trim(),
email: input.email.toLowerCase()
};
}
function formatUserData(user: ValidatedUser): ValidatedUser {
return {
name: user.name.trim(),
email: user.email.toLowerCase()
};
}
async function createUser(input: UserInput): Promise<User> {
const validatedUser = validateUserInput(input);
const formattedUser = formatUserData(validatedUser);
const savedUser = await userRepository.create(formattedUser);
await emailService.sendWelcomeEmail(savedUser.email);
return savedUser;
}型別優先的程式設計
// ✅ 先定義型別,再實作功能
interface PaymentMethod {
id: string;
type: "credit_card" | "paypal" | "bank_transfer";
isDefault: boolean;
}
interface PaymentRequest {
amount: number;
currency: string;
paymentMethodId: string;
orderId: string;
}
interface PaymentResult {
success: boolean;
transactionId?: string;
errorMessage?: string;
}
class PaymentService {
async processPayment(request: PaymentRequest): Promise<PaymentResult> {
try {
const paymentMethod = await this.getPaymentMethod(request.paymentMethodId);
const transaction = await this.createTransaction(request, paymentMethod);
return {
success: true,
transactionId: transaction.id
};
} catch (error) {
return {
success: false,
errorMessage: error instanceof Error ? error.message : "付款處理失敗"
};
}
}
private async getPaymentMethod(id: string): Promise<PaymentMethod> {
// 實作取得付款方式邏輯
throw new Error("Method not implemented");
}
private async createTransaction(
request: PaymentRequest,
method: PaymentMethod
): Promise<{ id: string }> {
// 實作建立交易邏輯
throw new Error("Method not implemented");
}
}錯誤處理策略
// ✅ 使用 Result 模式進行錯誤處理
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
class UserService {
async getUser(id: string): Promise<Result<User, string>> {
try {
if (!id) {
return { success: false, error: "使用者 ID 不能為空" };
}
const user = await userRepository.findById(id);
if (!user) {
return { success: false, error: "找不到指定的使用者" };
}
return { success: true, data: user };
} catch (error) {
logger.error("取得使用者資料失敗", { id, error });
return {
success: false,
error: "系統錯誤,請稍後再試"
};
}
}
}
// 使用 Result 模式
async function displayUser(userId: string): Promise<void> {
const result = await userService.getUser(userId);
if (result.success) {
console.log(`使用者姓名: ${result.data.name}`);
} else {
console.error(`錯誤: ${result.error}`);
}
}3.2 常見錯誤與最佳解法
錯誤 1:型別斷言濫用
// ❌ 危險的型別斷言
function processApiResponse(response: any): User {
return response as User; // 沒有驗證,可能導致執行時錯誤
}
// ✅ 使用型別守衛
function isValidUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
typeof (obj as User).id === "string" &&
typeof (obj as User).name === "string" &&
typeof (obj as User).email === "string"
);
}
function processApiResponse(response: unknown): User | null {
if (isValidUser(response)) {
return response;
}
logger.warn("API 回應格式不正確", response);
return null;
}錯誤 2:忽略 null/undefined 檢查
// ❌ 沒有處理 null/undefined
function getUserName(user: User | null): string {
return user.name; // 可能拋出錯誤
}
// ✅ 適當的 null 檢查
function getUserName(user: User | null): string {
return user?.name ?? "未知使用者";
}
// ✅ 使用型別縮小
function getUserName(user: User | null): string {
if (!user) {
return "未知使用者";
}
return user.name; // TypeScript 知道 user 不是 null
}錯誤 3:Promise 型別處理不當
// ❌ 未正確處理 Promise 型別
async function fetchUsers(): Promise<User[]> {
const response = fetch("/api/users");
return response.json(); // 型別不匹配
}
// ✅ 正確的 Promise 處理
async function fetchUsers(): Promise<User[]> {
try {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data: unknown = await response.json();
if (!Array.isArray(data)) {
throw new Error("API 回應格式不正確");
}
return data.filter(isValidUser);
} catch (error) {
logger.error("取得使用者列表失敗", error);
throw new Error("無法載入使用者資料");
}
}3.3 與第三方套件整合
React 專案整合
// types/react.types.ts
import { ReactNode } from "react";
export interface BaseProps {
children?: ReactNode;
className?: string;
testId?: string;
}
export interface ButtonProps extends BaseProps {
variant?: "primary" | "secondary" | "danger";
size?: "small" | "medium" | "large";
disabled?: boolean;
loading?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
// components/Button.tsx
import React from "react";
import { ButtonProps } from "@/types/react.types";
export const Button: React.FC<ButtonProps> = ({
children,
variant = "primary",
size = "medium",
disabled = false,
loading = false,
onClick,
className = "",
testId
}) => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (!disabled && !loading && onClick) {
onClick(event);
}
};
return (
<button
type="button"
className={`btn btn--${variant} btn--${size} ${className}`}
disabled={disabled || loading}
onClick={handleClick}
data-testid={testId}
>
{loading ? "載入中..." : children}
</button>
);
};
// hooks/useApi.ts
import { useState, useEffect } from "react";
interface UseApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
export function useApi<T>(
fetcher: () => Promise<T>,
dependencies: any[] = []
): UseApiState<T> {
const [state, setState] = useState<UseApiState<T>>({
data: null,
loading: true,
error: null
});
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setState(prev => ({ ...prev, loading: true, error: null }));
const result = await fetcher();
if (!cancelled) {
setState({ data: result, loading: false, error: null });
}
} catch (error) {
if (!cancelled) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error.message : "未知錯誤"
});
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, dependencies);
return state;
}Vue 3 專案整合
// types/vue.types.ts
export interface ComponentProps {
[key: string]: any;
}
export interface EmitEvents {
[key: string]: (...args: any[]) => void;
}
// composables/useCounter.ts
import { ref, computed, Ref } from "vue";
interface UseCounterReturn {
count: Ref<number>;
doubleCount: Ref<number>;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export function useCounter(initialValue: number = 0): UseCounterReturn {
const count = ref(initialValue);
const doubleCount = computed(() => count.value * 2);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
const reset = () => {
count.value = initialValue;
};
return {
count,
doubleCount,
increment,
decrement,
reset
};
}
// components/UserCard.vue
<template>
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button @click="handleEdit">編輯</button>
</div>
</template>
<script setup lang="ts">
interface Props {
user: User;
}
interface Emits {
(e: "edit", user: User): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const handleEdit = () => {
emit("edit", props.user);
};
</script>Node.js 專案整合
// types/express.types.ts
import { Request, Response, NextFunction } from "express";
export interface AuthenticatedRequest extends Request {
user?: User;
}
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message: string;
errors?: string[];
}
export type AsyncHandler = (
req: Request,
res: Response,
next: NextFunction
) => Promise<void>;
// middleware/auth.ts
import { Response, NextFunction } from "express";
import { AuthenticatedRequest } from "@/types/express.types";
export const authMiddleware = async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) {
res.status(401).json({
success: false,
message: "需要驗證令牌"
});
return;
}
const user = await verifyToken(token);
req.user = user;
next();
} catch (error) {
res.status(401).json({
success: false,
message: "無效的驗證令牌"
});
}
};
// controllers/userController.ts
import { Response } from "express";
import { AuthenticatedRequest, ApiResponse } from "@/types/express.types";
export const getProfile = async (
req: AuthenticatedRequest,
res: Response<ApiResponse<User>>
): Promise<void> => {
try {
if (!req.user) {
res.status(401).json({
success: false,
message: "未授權的請求"
});
return;
}
res.json({
success: true,
data: req.user,
message: "成功取得使用者資料"
});
} catch (error) {
res.status(500).json({
success: false,
message: "伺服器錯誤"
});
}
};本章重點回顧:
- 遵循單一職責原則,讓函數功能明確且易於測試
- 先定義型別再實作功能,確保型別安全
- 使用 Result 模式和型別守衛處理錯誤
- 不同框架有其特定的 TypeScript 整合模式
- 適當的錯誤處理和 null 檢查是必要的
下一章預告: 我們將通過具體的程式碼範例,展示 TypeScript 的實際應用。
4. 範例程式碼
4.1 Class 類別實作範例
基礎類別設計
// models/User.ts
export class User {
private _id: string;
private _name: string;
private _email: string;
private _createdAt: Date;
private _lastLoginAt: Date | null = null;
constructor(id: string, name: string, email: string) {
this._id = id;
this._name = this.validateName(name);
this._email = this.validateEmail(email);
this._createdAt = new Date();
}
// Getter 方法
get id(): string {
return this._id;
}
get name(): string {
return this._name;
}
get email(): string {
return this._email;
}
get createdAt(): Date {
return new Date(this._createdAt);
}
get lastLoginAt(): Date | null {
return this._lastLoginAt ? new Date(this._lastLoginAt) : null;
}
get isNewUser(): boolean {
return this._lastLoginAt === null;
}
// Setter 方法
set name(newName: string) {
this._name = this.validateName(newName);
}
set email(newEmail: string) {
this._email = this.validateEmail(newEmail);
}
// 公開方法
updateLastLogin(): void {
this._lastLoginAt = new Date();
}
getDaysSinceCreation(): number {
const now = new Date();
const diffTime = Math.abs(now.getTime() - this._createdAt.getTime());
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
toJSON(): UserJSON {
return {
id: this._id,
name: this._name,
email: this._email,
createdAt: this._createdAt.toISOString(),
lastLoginAt: this._lastLoginAt?.toISOString() || null,
isNewUser: this.isNewUser
};
}
// 私有驗證方法
private validateName(name: string): string {
if (!name || name.trim().length === 0) {
throw new Error("使用者姓名不能為空");
}
if (name.length > 50) {
throw new Error("使用者姓名不能超過 50 個字元");
}
return name.trim();
}
private validateEmail(email: string): string {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error("電子郵件格式不正確");
}
return email.toLowerCase();
}
// 靜態工廠方法
static fromJSON(json: UserJSON): User {
const user = new User(json.id, json.name, json.email);
user._createdAt = new Date(json.createdAt);
if (json.lastLoginAt) {
user._lastLoginAt = new Date(json.lastLoginAt);
}
return user;
}
}
// 相關型別定義
interface UserJSON {
id: string;
name: string;
email: string;
createdAt: string;
lastLoginAt: string | null;
isNewUser: boolean;
}繼承與多型範例
// models/BaseEntity.ts
export abstract class BaseEntity {
protected _id: string;
protected _createdAt: Date;
protected _updatedAt: Date;
constructor(id: string) {
this._id = id;
this._createdAt = new Date();
this._updatedAt = new Date();
}
get id(): string {
return this._id;
}
get createdAt(): Date {
return new Date(this._createdAt);
}
get updatedAt(): Date {
return new Date(this._updatedAt);
}
protected updateTimestamp(): void {
this._updatedAt = new Date();
}
// 抽象方法,子類必須實作
abstract validate(): boolean;
abstract toJSON(): Record<string, any>;
}
// models/Product.ts
export class Product extends BaseEntity {
private _name: string;
private _price: number;
private _category: ProductCategory;
private _isActive: boolean = true;
constructor(
id: string,
name: string,
price: number,
category: ProductCategory
) {
super(id);
this._name = name;
this._price = price;
this._category = category;
}
get name(): string {
return this._name;
}
get price(): number {
return this._price;
}
get category(): ProductCategory {
return this._category;
}
get isActive(): boolean {
return this._isActive;
}
updatePrice(newPrice: number): void {
if (newPrice <= 0) {
throw new Error("商品價格必須大於 0");
}
this._price = newPrice;
this.updateTimestamp();
}
deactivate(): void {
this._isActive = false;
this.updateTimestamp();
}
activate(): void {
this._isActive = true;
this.updateTimestamp();
}
// 實作抽象方法
validate(): boolean {
return (
this._name.length > 0 &&
this._price > 0 &&
Object.values(ProductCategory).includes(this._category)
);
}
toJSON(): ProductJSON {
return {
id: this._id,
name: this._name,
price: this._price,
category: this._category,
isActive: this._isActive,
createdAt: this._createdAt.toISOString(),
updatedAt: this._updatedAt.toISOString()
};
}
}
enum ProductCategory {
ELECTRONICS = "electronics",
CLOTHING = "clothing",
BOOKS = "books",
HOME = "home",
SPORTS = "sports"
}
interface ProductJSON {
id: string;
name: string;
price: number;
category: ProductCategory;
isActive: boolean;
createdAt: string;
updatedAt: string;
}4.2 Interface 介面範例
複雜介面設計
// interfaces/Repository.ts
export interface Repository<T, TKey = string> {
findById(id: TKey): Promise<T | null>;
findAll(filter?: RepositoryFilter<T>): Promise<T[]>;
create(entity: Omit<T, "id" | "createdAt" | "updatedAt">): Promise<T>;
update(id: TKey, updates: Partial<T>): Promise<T | null>;
delete(id: TKey): Promise<boolean>;
count(filter?: RepositoryFilter<T>): Promise<number>;
}
export interface RepositoryFilter<T> {
where?: Partial<T>;
orderBy?: {
field: keyof T;
direction: "asc" | "desc";
}[];
limit?: number;
offset?: number;
}
// interfaces/Service.ts
export interface CacheService {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<boolean>;
clear(): Promise<void>;
exists(key: string): Promise<boolean>;
}
export interface EmailService {
sendWelcomeEmail(to: string, userName: string): Promise<EmailResult>;
sendResetPasswordEmail(to: string, resetToken: string): Promise<EmailResult>;
sendNotificationEmail(
to: string,
subject: string,
content: string
): Promise<EmailResult>;
}
export interface EmailResult {
success: boolean;
messageId?: string;
error?: string;
}
// interfaces/Events.ts
export interface DomainEvent {
eventId: string;
eventType: string;
aggregateId: string;
timestamp: Date;
version: number;
data: Record<string, any>;
}
export interface EventHandler<T extends DomainEvent = DomainEvent> {
eventType: string;
handle(event: T): Promise<void>;
}
export interface EventBus {
publish<T extends DomainEvent>(event: T): Promise<void>;
subscribe<T extends DomainEvent>(handler: EventHandler<T>): void;
unsubscribe<T extends DomainEvent>(handler: EventHandler<T>): void;
}條件型別與映射型別
// types/Utility.ts
// 條件型別範例
type NonNullable<T> = T extends null | undefined ? never : T;
type ApiEndpoint<T> = T extends "users"
? "/api/users"
: T extends "products"
? "/api/products"
: T extends "orders"
? "/api/orders"
: never;
// 映射型別範例
type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;
// 實用工具型別
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type KeysOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
// 使用範例
interface UserProfile {
id: string;
name: string;
email: string;
avatar?: string;
settings: {
theme: "light" | "dark";
notifications: boolean;
};
}
type ReadOnlyUserProfile = ReadOnly<UserProfile>;
type PartialUserProfile = DeepPartial<UserProfile>;
type UserWithRequiredId = PartialExcept<UserProfile, "id">;
type StringKeys = KeysOfType<UserProfile, string>; // "id" | "name" | "email" | "avatar"
type RequiredUserKeys = RequiredKeys<UserProfile>; // "id" | "name" | "email" | "settings"4.3 泛型實作範例
泛型類別完整實作
// utils/Result.ts
export class Result<T, E = Error> {
private constructor(
private readonly _isSuccess: boolean,
private readonly _value?: T,
private readonly _error?: E
) {}
get isSuccess(): boolean {
return this._isSuccess;
}
get isFailure(): boolean {
return !this._isSuccess;
}
get value(): T {
if (!this._isSuccess) {
throw new Error("Cannot get value from failed result");
}
return this._value as T;
}
get error(): E {
if (this._isSuccess) {
throw new Error("Cannot get error from successful result");
}
return this._error as E;
}
// 靜態工廠方法
static success<T, E = Error>(value: T): Result<T, E> {
return new Result<T, E>(true, value);
}
static failure<T, E = Error>(error: E): Result<T, E> {
return new Result<T, E>(false, undefined, error);
}
// 函數式方法
map<U>(fn: (value: T) => U): Result<U, E> {
if (this._isSuccess) {
try {
return Result.success(fn(this._value as T));
} catch (error) {
return Result.failure(error as E);
}
}
return Result.failure(this._error as E);
}
flatMap<U>(fn: (value: T) => Result<U, E>): Result<U, E> {
if (this._isSuccess) {
return fn(this._value as T);
}
return Result.failure(this._error as E);
}
mapError<F>(fn: (error: E) => F): Result<T, F> {
if (this._isSuccess) {
return Result.success(this._value as T);
}
return Result.failure(fn(this._error as E));
}
// 條件檢查
match<U>(
onSuccess: (value: T) => U,
onFailure: (error: E) => U
): U {
if (this._isSuccess) {
return onSuccess(this._value as T);
}
return onFailure(this._error as E);
}
}
// 使用範例
async function fetchUser(id: string): Promise<Result<User, string>> {
try {
if (!id) {
return Result.failure("使用者 ID 不能為空");
}
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return Result.failure(`HTTP ${response.status}: ${response.statusText}`);
}
const userData = await response.json();
if (!isValidUser(userData)) {
return Result.failure("使用者資料格式不正確");
}
return Result.success(userData);
} catch (error) {
return Result.failure("網路請求失敗");
}
}
// 使用 Result
async function displayUserProfile(userId: string): Promise<void> {
const result = await fetchUser(userId);
result.match(
(user) => {
console.log(`歡迎,${user.name}!`);
updateUI(user);
},
(error) => {
console.error(`錯誤:${error}`);
showErrorMessage(error);
}
);
}進階泛型模式
// utils/EventEmitter.ts
export class TypedEventEmitter<TEvents extends Record<string, any[]>> {
private listeners: {
[K in keyof TEvents]?: Array<(...args: TEvents[K]) => void>;
} = {};
on<K extends keyof TEvents>(
event: K,
listener: (...args: TEvents[K]) => void
): this {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
return this;
}
off<K extends keyof TEvents>(
event: K,
listener: (...args: TEvents[K]) => void
): this {
const eventListeners = this.listeners[event];
if (eventListeners) {
const index = eventListeners.indexOf(listener);
if (index > -1) {
eventListeners.splice(index, 1);
}
}
return this;
}
emit<K extends keyof TEvents>(event: K, ...args: TEvents[K]): boolean {
const eventListeners = this.listeners[event];
if (eventListeners && eventListeners.length > 0) {
eventListeners.forEach(listener => {
try {
listener(...args);
} catch (error) {
console.error(`Event listener error for ${String(event)}:`, error);
}
});
return true;
}
return false;
}
removeAllListeners<K extends keyof TEvents>(event?: K): this {
if (event) {
delete this.listeners[event];
} else {
this.listeners = {};
}
return this;
}
listenerCount<K extends keyof TEvents>(event: K): number {
return this.listeners[event]?.length ?? 0;
}
}
// 定義事件型別
interface AppEvents {
userLogin: [user: User];
userLogout: [userId: string];
productAdded: [product: Product];
error: [error: Error, context?: string];
statusChange: [status: "loading" | "ready" | "error"];
}
// 使用型別化事件發射器
const eventBus = new TypedEventEmitter<AppEvents>();
// 型別安全的事件監聽
eventBus.on("userLogin", (user) => {
// TypeScript 知道 user 的型別是 User
console.log(`使用者 ${user.name} 已登入`);
});
eventBus.on("error", (error, context) => {
// TypeScript 知道參數型別
console.error(`錯誤發生 [${context}]:`, error.message);
});
// 型別安全的事件發射
eventBus.emit("userLogin", currentUser);
eventBus.emit("statusChange", "loading");4.4 模組化範例
ES 模組系統
// utils/validators.ts
export interface ValidationRule<T> {
validate(value: T): boolean;
message: string;
}
export class RequiredRule implements ValidationRule<any> {
message = "此欄位為必填";
validate(value: any): boolean {
return value !== null && value !== undefined && value !== "";
}
}
export class EmailRule implements ValidationRule<string> {
message = "請輸入有效的電子郵件地址";
validate(value: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
}
}
export class MinLengthRule implements ValidationRule<string> {
constructor(private minLength: number) {
this.message = `長度不能少於 ${minLength} 個字元`;
}
message: string;
validate(value: string): boolean {
return value.length >= this.minLength;
}
}
// utils/validator.ts
import { ValidationRule } from "./validators";
export interface ValidationResult {
isValid: boolean;
errors: string[];
}
export class Validator<T> {
private rules: Map<keyof T, ValidationRule<any>[]> = new Map();
addRule<K extends keyof T>(
field: K,
rule: ValidationRule<T[K]>
): this {
if (!this.rules.has(field)) {
this.rules.set(field, []);
}
this.rules.get(field)!.push(rule);
return this;
}
validate(data: T): ValidationResult {
const errors: string[] = [];
for (const [field, rules] of this.rules.entries()) {
const value = data[field];
for (const rule of rules) {
if (!rule.validate(value)) {
errors.push(`${String(field)}: ${rule.message}`);
}
}
}
return {
isValid: errors.length === 0,
errors
};
}
}
// services/UserService.ts
import { Validator } from "@/utils/validator";
import { RequiredRule, EmailRule, MinLengthRule } from "@/utils/validators";
import { Repository } from "@/interfaces/Repository";
import { Result } from "@/utils/Result";
export class UserService {
private validator: Validator<CreateUserRequest>;
constructor(
private userRepository: Repository<User>,
private emailService: EmailService
) {
this.setupValidator();
}
private setupValidator(): void {
this.validator = new Validator<CreateUserRequest>()
.addRule("name", new RequiredRule())
.addRule("name", new MinLengthRule(2))
.addRule("email", new RequiredRule())
.addRule("email", new EmailRule())
.addRule("password", new RequiredRule())
.addRule("password", new MinLengthRule(8));
}
async createUser(request: CreateUserRequest): Promise<Result<User, string>> {
// 驗證輸入資料
const validationResult = this.validator.validate(request);
if (!validationResult.isValid) {
return Result.failure(validationResult.errors.join(", "));
}
try {
// 檢查電子郵件是否已存在
const existingUser = await this.userRepository.findAll({
where: { email: request.email }
});
if (existingUser.length > 0) {
return Result.failure("此電子郵件已被註冊");
}
// 建立新使用者
const newUser = await this.userRepository.create({
name: request.name,
email: request.email,
password: await this.hashPassword(request.password)
});
// 發送歡迎信件
await this.emailService.sendWelcomeEmail(newUser.email, newUser.name);
return Result.success(newUser);
} catch (error) {
return Result.failure("建立使用者失敗");
}
}
private async hashPassword(password: string): Promise<string> {
// 實作密碼雜湊邏輯
return `hashed_${password}`;
}
}
interface CreateUserRequest {
name: string;
email: string;
password: string;
}本章重點回顧:
- 類別設計應遵循封裝原則,適當使用 private、protected、public
- 介面設計要考慮擴展性和重用性
- 泛型讓程式碼更加靈活,但要保持型別安全
- 模組化設計有助於程式碼組織和測試
- 實用工具型別能大幅提升開發效率
下一章預告: 我們將探討 TypeScript 的實務應用建議和團隊協作最佳實踐。
5. 實務建議
5.1 善用 TypeScript 的型別檢查
編譯器嚴格模式設定
啟用嚴格模式能幫助捕捉更多潛在錯誤:
// tsconfig.json 嚴格設定
{
"compilerOptions": {
"strict": true, // 啟用所有嚴格檢查
"noImplicitAny": true, // 禁止隱式 any
"strictNullChecks": true, // 嚴格 null 檢查
"strictFunctionTypes": true, // 嚴格函數型別檢查
"strictBindCallApply": true, // 嚴格檢查 bind/call/apply
"strictPropertyInitialization": true, // 嚴格屬性初始化檢查
"noImplicitThis": true, // 禁止隱式 this
"noImplicitReturns": true, // 檢查函數所有路徑都有回傳值
"noFallthroughCasesInSwitch": true, // 檢查 switch case 是否遺漏 break
"noUncheckedIndexedAccess": true, // 檢查索引存取
"exactOptionalPropertyTypes": true, // 精確可選屬性型別
"noImplicitOverride": true, // 需要明確標記 override
"noPropertyAccessFromIndexSignature": true, // 禁止從索引簽名存取屬性
"noUnusedLocals": true, // 檢查未使用的區域變數
"noUnusedParameters": true // 檢查未使用的參數
}
}使用型別斷言的最佳實踐
// ❌ 避免不安全的型別斷言
function processData(data: unknown): User {
return data as User; // 危險:沒有驗證
}
// ✅ 使用型別守衛
function isUser(data: unknown): data is User {
return (
typeof data === "object" &&
data !== null &&
typeof (data as User).id === "string" &&
typeof (data as User).name === "string" &&
typeof (data as User).email === "string"
);
}
function processData(data: unknown): User | null {
if (isUser(data)) {
return data; // TypeScript 知道這是 User 型別
}
return null;
}
// ✅ 使用 zod 等函式庫進行執行時驗證
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number().min(0).max(150)
});
type User = z.infer<typeof UserSchema>;
function validateAndParseUser(data: unknown): User {
return UserSchema.parse(data); // 會拋出錯誤如果驗證失敗
}
function safeParseUser(data: unknown): { success: true; data: User } | { success: false; error: string } {
const result = UserSchema.safeParse(data);
if (result.success) {
return { success: true, data: result.data };
}
return { success: false, error: result.error.message };
}利用編譯時檢查防止錯誤
// 使用品牌型別防止混用相似型別
type UserId = string & { readonly brand: unique symbol };
type ProductId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createProductId(id: string): ProductId {
return id as ProductId;
}
function getUserById(id: UserId): Promise<User> {
// 實作...
}
function getProductById(id: ProductId): Promise<Product> {
// 實作...
}
// 編譯時防止錯誤
const userId = createUserId("user-123");
const productId = createProductId("product-456");
getUserById(userId); // ✅ 正確
getUserById(productId); // ❌ 編譯錯誤
// 使用常數斷言和映射型別
const THEMES = ["light", "dark", "auto"] as const;
type Theme = typeof THEMES[number]; // "light" | "dark" | "auto"
const STATUS_CODES = {
OK: 200,
NOT_FOUND: 404,
INTERNAL_ERROR: 500
} as const;
type StatusCode = typeof STATUS_CODES[keyof typeof STATUS_CODES]; // 200 | 404 | 5005.2 除錯與測試
TypeScript 除錯設定
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug TypeScript",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/src/index.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"],
"env": {
"NODE_ENV": "development"
}
},
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "--no-cache"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
}
]
}單元測試最佳實踐
// tests/UserService.test.ts
import { UserService } from "../src/services/UserService";
import { MockUserRepository } from "./mocks/MockUserRepository";
import { MockEmailService } from "./mocks/MockEmailService";
describe("UserService", () => {
let userService: UserService;
let mockUserRepository: MockUserRepository;
let mockEmailService: MockEmailService;
beforeEach(() => {
mockUserRepository = new MockUserRepository();
mockEmailService = new MockEmailService();
userService = new UserService(mockUserRepository, mockEmailService);
});
describe("createUser", () => {
it("should create user successfully with valid data", async () => {
// Arrange
const validRequest = {
name: "王小明",
email: "wang@example.com",
password: "password123"
};
mockUserRepository.findAll.mockResolvedValue([]);
mockUserRepository.create.mockResolvedValue({
id: "user-1",
...validRequest,
createdAt: new Date()
});
// Act
const result = await userService.createUser(validRequest);
// Assert
expect(result.isSuccess).toBe(true);
if (result.isSuccess) {
expect(result.value.name).toBe(validRequest.name);
expect(result.value.email).toBe(validRequest.email);
}
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
validRequest.email,
validRequest.name
);
});
it("should fail with invalid email", async () => {
// Arrange
const invalidRequest = {
name: "王小明",
email: "invalid-email",
password: "password123"
};
// Act
const result = await userService.createUser(invalidRequest);
// Assert
expect(result.isFailure).toBe(true);
if (result.isFailure) {
expect(result.error).toContain("請輸入有效的電子郵件地址");
}
});
it("should fail when email already exists", async () => {
// Arrange
const existingUserRequest = {
name: "李小華",
email: "existing@example.com",
password: "password123"
};
mockUserRepository.findAll.mockResolvedValue([
{ id: "existing-user", name: "現有用戶", email: "existing@example.com" }
]);
// Act
const result = await userService.createUser(existingUserRequest);
// Assert
expect(result.isFailure).toBe(true);
if (result.isFailure) {
expect(result.error).toBe("此電子郵件已被註冊");
}
});
});
});
// tests/mocks/MockUserRepository.ts
import { Repository, RepositoryFilter } from "../../src/interfaces/Repository";
import { User } from "../../src/models/User";
export class MockUserRepository implements Repository<User> {
findById = jest.fn<Promise<User | null>, [string]>();
findAll = jest.fn<Promise<User[]>, [RepositoryFilter<User>?]>();
create = jest.fn<Promise<User>, [Omit<User, "id" | "createdAt" | "updatedAt">]>();
update = jest.fn<Promise<User | null>, [string, Partial<User>]>();
delete = jest.fn<Promise<boolean>, [string]>();
count = jest.fn<Promise<number>, [RepositoryFilter<User>?]>();
}型別測試
// tests/types.test.ts
import { expectType, expectAssignable, expectNotAssignable } from "tsd";
import { User, CreateUserRequest } from "../src/types/user";
import { Result } from "../src/utils/Result";
// 測試型別正確性
expectType<string>(new User("1", "Test", "test@example.com").id);
expectType<Date>(new User("1", "Test", "test@example.com").createdAt);
// 測試型別相容性
expectAssignable<CreateUserRequest>({
name: "Test User",
email: "test@example.com",
password: "password123"
});
// 測試型別不相容
expectNotAssignable<CreateUserRequest>({
name: "Test User",
// 缺少 email 和 password
});
// 測試 Result 型別
expectType<Result<User, string>>(Result.success(new User("1", "Test", "test@example.com")));
expectType<Result<User, string>>(Result.failure("Error message"));5.3 團隊協作與程式風格統一
ESLint 與 Prettier 設定
// .eslintrc.json
{
"extends": [
"@typescript-eslint/recommended",
"@typescript-eslint/recommended-requiring-type-checking",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint", "import"],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
]
}
}// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}Git Hooks 設定
// package.json
{
"scripts": {
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"type-check": "tsc --noEmit",
"format": "prettier --write src/**/*.ts",
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "npm run pre-commit",
"pre-push": "npm run type-check && npm test"
}
}
}程式碼審查檢查清單
型別安全性檢查:
- 避免使用
any型別,使用具體型別或unknown - 適當處理
null和undefined - 使用型別守衛而非型別斷言
- 泛型使用是否合理且有型別約束
程式碼品質檢查:
- 函數職責是否單一且明確
- 變數和函數命名是否清楚描述其用途
- 是否有適當的錯誤處理
- 程式碼是否遵循 SOLID 原則
效能考量:
- 避免不必要的型別計算
- 合理使用
readonly和const assertions - 避免深層嵌套的條件型別
測試覆蓋:
- 關鍵功能是否有單元測試
- 邊界條件是否有測試案例
- 型別定義是否有對應的型別測試
文件化最佳實踐
/**
* 使用者服務類別,負責處理使用者相關的業務邏輯
*
* @example
* ```typescript
* const userService = new UserService(userRepository, emailService);
* const result = await userService.createUser({
* name: "王小明",
* email: "wang@example.com",
* password: "securePassword123"
* });
*
* if (result.isSuccess) {
* console.log("使用者建立成功:", result.value.name);
* }
* ```
*/
export class UserService {
/**
* 建立新使用者
*
* @param request - 建立使用者的請求資料
* @returns Promise<Result<User, string>> - 包含建立結果的 Promise
*
* @throws 不會拋出例外,所有錯誤都包裝在 Result 中
*
* @example
* ```typescript
* const result = await userService.createUser({
* name: "李小華",
* email: "lee@example.com",
* password: "password123"
* });
* ```
*/
async createUser(request: CreateUserRequest): Promise<Result<User, string>> {
// 實作...
}
}
/**
* 建立使用者的請求介面
*
* @interface CreateUserRequest
*/
interface CreateUserRequest {
/** 使用者姓名,長度必須在 2-50 字元之間 */
name: string;
/** 電子郵件地址,必須符合有效的電子郵件格式 */
email: string;
/** 密碼,長度必須至少 8 個字元 */
password: string;
}本章重點回顧:
- 啟用 TypeScript 嚴格模式以獲得最大的型別安全性
- 使用型別守衛和執行時驗證替代不安全的型別斷言
- 建立完整的測試策略,包括單元測試和型別測試
- 使用 ESLint、Prettier 和 Git Hooks 統一程式碼風格
- 良好的文件化有助於團隊協作和程式碼維護
6. 檢查清單
6.1 專案建立檢查清單
初始設定
安裝 TypeScript
npm install -D typescript @types/node建立 tsconfig.json
npx tsc --init設定專案目錄結構
src/ ├── components/ ├── services/ ├── types/ ├── utils/ └── index.ts安裝開發工具
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin npm install -D prettier eslint-config-prettier npm install -D husky lint-staged
設定檔配置
tsconfig.json 嚴格模式
-
"strict": true -
"noImplicitAny": true -
"strictNullChecks": true -
"noImplicitReturns": true
-
ESLint 設定
- TypeScript 專用規則
- Import 排序規則
- 禁用
any型別警告
Git Hooks 設定
- pre-commit: 程式碼格式化和檢查
- pre-push: 型別檢查和測試
6.2 開發期間檢查清單
型別定義
介面和型別
- 所有公開介面都有完整的型別定義
- 使用
interface定義物件結構 - 使用
type定義聯合型別和複雜型別 - 適當使用泛型增加重用性
函數定義
- 明確指定參數型別
- 明確指定回傳型別
- 處理可能的
null和undefined - 使用 Result 模式處理錯誤
程式碼品質
命名規範
- 變數使用 camelCase
- 類別使用 PascalCase
- 常數使用 UPPER_SNAKE_CASE
- 檔案命名一致性
函數設計
- 單一職責原則
- 函數長度適中(< 20 行)
- 避免深層嵌套(< 3 層)
- 有意義的函數名稱
錯誤處理
- 防禦性程式設計
- 輸入驗證
- 邊界條件檢查
- 適當的例外處理
- 有意義的錯誤訊息
6.3 程式碼審查檢查清單
型別安全性
型別檢查
- 沒有使用
any型別 - 正確處理
null和undefined - 使用型別守衛而非型別斷言
- 泛型使用合理且有約束
- 沒有使用
API 整合
- 外部 API 資料有型別驗證
- 使用 unknown 處理未知資料
- 適當的錯誤處理機制
效能和維護性
程式碼組織
- 模組化設計
- 循環相依檢查
- 合理的檔案大小
- 清楚的目錄結構
測試覆蓋
- 關鍵功能有單元測試
- 邊界條件測試
- Mock 使用適當
- 測試案例命名清楚
6.4 部署前檢查清單
建置和測試
編譯檢查
npm run type-check # TypeScript 編譯檢查 npm run lint # ESLint 檢查 npm run test # 單元測試 npm run build # 建置檢查程式碼品質
- 所有 ESLint 警告已解決
- 程式碼格式化一致
- 沒有 TODO 或 FIXME 註解
- 移除 console.log 和除錯程式碼
文件和註解
文件化
- API 文件已更新
- README 包含最新的安裝和使用說明
- 重要函數有 JSDoc 註解
- 型別定義有適當的註解
版本控制
- 提交訊息清楚描述變更
- 沒有敏感資訊在程式碼中
- 分支命名符合規範
- Pull Request 描述完整
6.5 常見問題快速檢查
編譯錯誤
型別錯誤排查
- 檢查變數型別定義
- 確認函數參數和回傳型別
- 檢查 null/undefined 處理
- 確認模組 import/export
設定問題
- tsconfig.json 路徑設定
- 模組解析設定
- 編譯目標版本
- include/exclude 設定
執行時錯誤
- 資料驗證
- API 回應格式檢查
- 輸入資料驗證
- 型別守衛實作
- 錯誤處理邏輯
總結
這份檢查清單涵蓋了 TypeScript 專案從建立到部署的完整流程。建議將此清單加入到團隊的開發流程中,確保程式碼品質和型別安全性。
定期檢視和更新這些檢查項目,隨著專案成長和團隊經驗累積,持續改善開發流程和程式碼品質。
7. 進階主題
7.1 條件型別 (Conditional Types)
條件型別允許我們根據型別條件來選擇不同的型別,類似於程式中的三元運算子:
// 基本條件型別語法
type ConditionalType<T> = T extends string ? string[] : number[];
type Example1 = ConditionalType<string>; // string[]
type Example2 = ConditionalType<number>; // number[]
// 實用的條件型別範例
type NonNullable<T> = T extends null | undefined ? never : T;
type ApiResponse<T> = T extends string
? { message: T }
: T extends number
? { code: T }
: { data: T };
// 分散式條件型別
type ToArray<T> = T extends any ? T[] : never;
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]高級條件型別模式
// 提取型別資訊
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type GetFirstParameter<T> = T extends (first: infer P, ...args: any[]) => any ? P : never;
// 使用範例
function processUser(user: User): Promise<User> {
return Promise.resolve(user);
}
type ReturnType = GetReturnType<typeof processUser>; // Promise<User>
type FirstParam = GetFirstParameter<typeof processUser>; // User
// 遞迴條件型別
type Flatten<T> = T extends readonly (infer U)[]
? Flatten<U>
: T;
type DeepArray = number[][][];
type Flattened = Flatten<DeepArray>; // number
// 字串操作型別
type Capitalize<S extends string> = S extends `${infer F}${infer R}`
? `${Uppercase<F>}${R}`
: S;
type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}`
? `${P1}${Capitalize<CamelCase<`${P2}${P3}`>>}`
: S;
type Example = CamelCase<"user_profile_data">; // "userProfileData"7.2 映射型別 (Mapped Types)
映射型別讓我們能基於現有型別創建新的型別:
// 基本映射型別
type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
// 進階映射型別
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface User {
name: string;
age: number;
email: string;
}
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }
// 條件映射型別
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P];
};
type StringFields = PickByType<User, string>; // { name: string; email: string; }
// 模板字面量型別映射
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type UserEventHandlers = EventHandlers<User>;
// {
// onName: (value: string) => void;
// onAge: (value: number) => void;
// onEmail: (value: string) => void;
// }7.3 模板字面型別 (Template Literal Types)
模板字面量型別允許我們使用模板字面量語法來操作字串型別:
// 基本模板字面量型別
type Greeting = `Hello, ${string}!`;
type ApiEndpoint = `/api/${string}`;
// 結合聯合型別
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = "users" | "products" | "orders";
type ApiCall = `${HttpMethod} /api/${ApiRoute}`;
// 實際使用範例
interface ApiConfig {
[K in ApiCall]: {
url: string;
handler: Function;
};
}
// 動態屬性名稱
type EventType = "click" | "hover" | "focus";
type ElementId = "button" | "input" | "form";
type EventHandler = {
[K in `${ElementId}_${EventType}`]: (event: Event) => void;
};
// CSS 樣式型別
type CSSUnit = "px" | "em" | "rem" | "%";
type CSSProperty = "margin" | "padding" | "width" | "height";
type CSSValue = `${number}${CSSUnit}`;
type CSSStyles = {
[K in CSSProperty]: CSSValue;
};
// 路由型別系統
type RouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & RouteParams<Rest>
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type UserRouteParams = RouteParams<"/users/:id/posts/:postId">;
// { id: string; postId: string; }7.4 裝飾器 (Decorators)
裝飾器是一種特殊的聲明,可以附加到類別、方法、存取器、屬性或參數上。裝飾器使用 @expression 語法。
重要提示: TypeScript 5.0+ 已支援 TC39 Stage 3 裝飾器(ECMAScript 標準裝飾器)。舊版的實驗性裝飾器(
experimentalDecorators)仍可使用,但建議新專案優先採用標準裝飾器。TypeScript 7.0 原生編譯器目前不支援編譯舊版裝飾器語法。
// 標準裝飾器(TC39 Stage 3,推薦)
// 不需要在 tsconfig.json 中設定 experimentalDecorators
// 類別裝飾器
function sealed(target: Function, context: ClassDecoratorContext) {
Object.seal(target);
Object.seal(target.prototype);
}
@sealed
class BugReport {
type = "report";
title: string;
constructor(title: string) {
this.title = title;
}
}
// 方法裝飾器
function log(target: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`呼叫方法 ${methodName},參數:`, args);
const result = target.call(this, ...args);
console.log(`方法 ${methodName} 回傳:`, result);
return result;
}
return replacementMethod;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
// 自動存取器裝飾器(accessor 關鍵字)
function range(min: number, max: number) {
return function (target: ClassAccessorDecoratorTarget<any, number>, context: ClassAccessorDecoratorContext) {
return {
set(value: number) {
if (value < min || value > max) {
throw new RangeError(`值必須在 ${min} 和 ${max} 之間`);
}
target.set.call(this, value);
},
get() {
return target.get.call(this);
},
init(value: number) {
if (value < min || value > max) {
throw new RangeError(`初始值必須在 ${min} 和 ${max} 之間`);
}
return value;
}
};
};
}
class Temperature {
@range(-273, 1000)
accessor celsius: number = 0;
}舊版實驗性裝飾器
若專案仍需使用舊版裝飾器(如 Angular 等框架),需在 tsconfig.json 中啟用:
// tsconfig.json 設定(舊版裝飾器)
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
// 類別裝飾器
function Component(target: any) {
// 為類別添加額外的功能
target.prototype.render = function() {
return `<div>${this.name}</div>`;
};
}
@Component
class MyComponent {
name = "Hello World";
}
// 方法裝飾器
function Log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`呼叫方法 ${propertyName},參數:`, args);
const result = method.apply(this, args);
console.log(`方法 ${propertyName} 回傳:`, result);
return result;
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
// 屬性裝飾器
function Required(target: any, propertyName: string) {
// 標記必填欄位
const requiredFields = target.constructor.requiredFields || [];
requiredFields.push(propertyName);
target.constructor.requiredFields = requiredFields;
}
class User {
@Required
name: string;
@Required
email: string;
age?: number;
}
// 參數裝飾器
function Validate(target: any, propertyName: string, parameterIndex: number) {
// 記錄需要驗證的參數
console.log(`參數 ${parameterIndex} 在方法 ${propertyName} 中需要驗證`);
}
class UserService {
createUser(@Validate name: string, @Validate email: string) {
// 建立使用者邏輯
}
}實際應用範例
// 快取裝飾器
function Cache(ttl: number = 60000) { // 預設快取 1 分鐘
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const cache = new Map();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
const now = Date.now();
if (cache.has(key)) {
const { data, timestamp } = cache.get(key);
if (now - timestamp < ttl) {
return data;
}
}
const result = originalMethod.apply(this, args);
cache.set(key, { data: result, timestamp: now });
return result;
};
};
}
// 使用快取裝飾器
class ApiService {
@Cache(30000) // 快取 30 秒
async fetchUserData(userId: string): Promise<User> {
// 實際的 API 呼叫
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
}8. 效能優化與最佳實踐
8.1 編譯效能優化
專案組態最佳化
// tsconfig.json 效能設定
{
"compilerOptions": {
// 跳過型別定義檔案檢查以提升速度
"skipLibCheck": true,
// 只檢查實際使用的檔案
"skipDefaultLibCheck": true,
// 增量編譯
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
// 專案參考(大型專案)
"composite": true,
"declaration": true,
"declarationMap": true,
// 模組解析優化
"moduleResolution": "bundler",
"esModuleInterop": true,
// 注意:TS 6.0 中 allowSyntheticDefaultImports 已始終啟用,無需設定
// 減少型別檢查負擔
"isolatedModules": true,
"noEmit": true
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}專案參考 (Project References)
// 根目錄 tsconfig.json
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./packages/utils" }
]
}
// packages/core/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src/**/*"],
"references": [
{ "path": "../utils" }
]
}8.2 型別檢查效能
避免複雜的型別運算
// ❌ 複雜的遞迴型別(可能導致編譯緩慢)
type DeepFlatten<T> = T extends readonly (infer U)[]
? U extends readonly (infer V)[]
? V extends readonly (infer W)[]
? W extends readonly (infer X)[]
? X extends readonly (infer Y)[]
? Y
: X
: W
: V
: U
: T;
// ✅ 簡化或使用適當的深度限制
type Flatten<T, Depth extends number = 5> =
Depth extends 0
? T
: T extends readonly (infer U)[]
? Flatten<U, Prev<Depth>>
: T;
type Prev<T extends number> = T extends 0 ? 0 : T extends 1 ? 0 : T extends 2 ? 1 : T extends 3 ? 2 : T extends 4 ? 3 : T extends 5 ? 4 : number;使用型別快取
// 使用 type alias 快取複雜型別
type CachedComplexType<T> = {
[K in keyof T]: T[K] extends Function
? T[K]
: T[K] extends object
? CachedComplexType<T[K]>
: T[K];
};
// 一次計算,多次使用
type ProcessedUser = CachedComplexType<User>;
type ProcessedProduct = CachedComplexType<Product>;8.3 打包優化
型別守衛最佳化
// ✅ 使用高效的型別守衛
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
"id" in obj &&
"name" in obj &&
"email" in obj
);
}
// ✅ 使用 discriminated unions 提升效能
interface LoadingState {
status: "loading";
}
interface SuccessState {
status: "success";
data: any;
}
interface ErrorState {
status: "error";
error: string;
}
type AsyncState = LoadingState | SuccessState | ErrorState;
function handleState(state: AsyncState) {
// TypeScript 能有效地進行型別縮小
switch (state.status) {
case "loading":
// 只有 LoadingState 的屬性可用
break;
case "success":
// 只有 SuccessState 的屬性可用
console.log(state.data);
break;
case "error":
// 只有 ErrorState 的屬性可用
console.error(state.error);
break;
}
}避免過度的型別運算
// ❌ 每次都重新計算型別
function processData<T>(data: T): Omit<T, "id"> & { processedAt: Date } {
// 這會在每次調用時重新計算型別
const { id, ...rest } = data as any;
return { ...rest, processedAt: new Date() };
}
// ✅ 預先定義好型別
type ProcessedData<T> = Omit<T, "id"> & { processedAt: Date };
function processData<T>(data: T): ProcessedData<T> {
const { id, ...rest } = data as any;
return { ...rest, processedAt: new Date() };
}8.4 記憶體使用優化
適當使用 readonly
// 使用 readonly 減少不必要的防禦性複製
interface ReadOnlyConfig {
readonly apiUrl: string;
readonly timeout: number;
readonly retryCount: number;
readonly endpoints: readonly string[];
}
// 使用 const assertions
const themes = ["light", "dark", "auto"] as const;
const config = {
theme: "light",
version: "1.0.0"
} as const;型別收窄策略
// ✅ 使用聯合型別而非泛型(當適合時)
type ApiResult =
| { success: true; data: any }
| { success: false; error: string };
// ✅ 而非過度泛型化
interface ApiResult<T, E = string> {
success: boolean;
data?: T;
error?: E;
}9. TypeScript 生態系統
9.1 開發工具
VS Code 設定優化
// .vscode/settings.json
{
"typescript.preferences.quoteStyle": "double",
"typescript.preferences.semicolons": "insert",
"typescript.suggest.autoImports": true,
"typescript.suggest.paths": true,
"typescript.preferences.organizeImports": true,
"typescript.inlayHints.parameterNames.enabled": "all",
"typescript.inlayHints.variableTypes.enabled": true,
"typescript.inlayHints.functionLikeReturnTypes.enabled": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll.eslint": true
},
"typescript.updateImportsOnFileMove.enabled": "always"
}推薦的 VS Code 擴充功能
// .vscode/extensions.json
{
"recommendations": [
"ms-vscode.vscode-typescript-next",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-json",
"christian-kohler.path-intellisense",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}9.2 建置工具
Webpack 整合
// webpack.config.ts
import { Configuration } from "webpack";
import path from "path";
const config: Configuration = {
mode: "development",
entry: "./src/index.ts",
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: "ts-loader",
options: {
configFile: path.resolve(__dirname, "tsconfig.json"),
transpileOnly: true, // 只轉譯,型別檢查交給 fork-ts-checker-webpack-plugin
},
},
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
alias: {
"@": path.resolve(__dirname, "src"),
},
},
plugins: [
new (require("fork-ts-checker-webpack-plugin"))({
typescript: {
configFile: path.resolve(__dirname, "tsconfig.json"),
},
}),
],
};
export default config;Vite 整合
// vite.config.ts
import { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
build: {
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "MyLibrary",
fileName: (format) => `my-library.${format}.js`,
},
rollupOptions: {
external: ["vue", "react"],
output: {
globals: {
vue: "Vue",
react: "React",
},
},
},
},
esbuild: {
target: "esnext",
},
});9.3 測試框架
Jest 設定
// jest.config.ts
import type { Config } from "@jest/types";
const config: Config.InitialOptions = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/src"],
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
transform: {
"^.+\\.ts$": "ts-jest",
},
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.d.ts",
"!src/**/*.test.ts",
"!src/**/*.spec.ts",
],
moduleNameMapping: {
"^@/(.*)$": "<rootDir>/src/$1",
},
setupFilesAfterEnv: ["<rootDir>/src/test/setup.ts"],
};
export default config;Vitest 設定
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { resolve } from "path";
export default defineConfig({
test: {
environment: "node",
globals: true,
setupFiles: ["./src/test/setup.ts"],
},
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
});9.4 型別定義管理
自訂型別定義
// types/global.d.ts
declare global {
interface Window {
gtag: (command: string, targetId: string, config?: any) => void;
dataLayer: any[];
}
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production" | "test";
API_BASE_URL: string;
DATABASE_URL: string;
}
}
}
// 模組擴展
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare module "*.svg" {
const content: string;
export default content;
}
// 第三方套件型別擴展
declare module "lodash" {
interface LoDashStatic {
customMethod(value: any): any;
}
}套件型別管理
// types/api.d.ts - API 回應型別統一管理
declare namespace API {
interface BaseResponse {
success: boolean;
message: string;
timestamp: string;
}
interface PaginatedResponse<T> extends BaseResponse {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
hasNext: boolean;
hasPrev: boolean;
};
}
namespace User {
interface Detail {
id: string;
name: string;
email: string;
avatar?: string;
createdAt: string;
updatedAt: string;
}
interface CreateRequest {
name: string;
email: string;
password: string;
}
interface UpdateRequest {
name?: string;
avatar?: string;
}
interface ListResponse extends PaginatedResponse<Detail> {}
}
}10. 常見問題與解決方案
10.1 編譯錯誤
模組解析問題
// 問題:Cannot find module '@/components/Button' or its corresponding type declarations
// 解決方案 1:檢查 tsconfig.json 路徑設定(TS 6.0 已棄用 baseUrl,直接在 paths 中使用完整路徑)
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"]
}
}
}
// 解決方案 2:確認 types 設定(TS 6.0 預設 types: [])
{
"compilerOptions": {
"types": ["node"] // 明確列出需要的 @types 套件
}
}
// 解決方案 2:確認檔案結構
src/
├── components/
│ ├── Button.tsx
│ └── index.ts // 統一匯出
// 解決方案 3:明確的副檔名
import { Button } from "@/components/Button"; // ✅
// 或
import { Button } from "@/components/Button.js"; // ✅ (在某些配置下)型別定義衝突
// 問題:Type 'string' is not assignable to type 'never'
// 原因:聯合型別縮小失敗
function processValue(value: string | number) {
if (typeof value === "string") {
// 在某些複雜情況下,TypeScript 可能無法正確縮小型別
return value.toUpperCase(); // 錯誤:value 被推斷為 never
}
}
// 解決方案:明確的型別斷言或重構
function processValue(value: string | number): string {
if (typeof value === "string") {
return (value as string).toUpperCase(); // 明確斷言
}
return value.toString().toUpperCase();
}
// 更好的解決方案:使用函數多載
function processValue(value: string): string;
function processValue(value: number): string;
function processValue(value: string | number): string {
return typeof value === "string" ? value.toUpperCase() : value.toString().toUpperCase();
}10.2 型別推斷問題
編譯速度緩慢
# 問題診斷
npx tsc --listFiles > compilation-files.txt
npx tsc --traceResolution > trace.txt
npx tsc --generateTrace trace-output解決方案 1:啟用增量編譯
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo"
}
}解決方案 2:排除不必要的檔案
{
"exclude": [
"node_modules",
"dist",
"coverage",
"**/*.test.ts",
"**/*.spec.ts",
"**/temp/**"
]
}解決方案 3:使用專案參考
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true
},
"references": [
{ "path": "./shared" }
]
}記憶體使用過高
// 問題:複雜的型別運算導致記憶體使用過高
// ❌ 避免過度複雜的遞迴型別
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
// ✅ 限制遞迴深度
type DeepReadonly<T, Depth extends number = 3> = Depth extends 0
? T
: {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P], Prev<Depth>>
: T[P];
};
// ✅ 或使用更簡單的替代方案
type SimpleReadonly<T> = {
readonly [P in keyof T]: T[P];
};10.3 執行時期問題
型別與執行時期資料不匹配
// 問題:API 回應與型別定義不匹配
// ❌ 危險的直接型別斷言
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data as User; // 沒有驗證
}
// ✅ 使用執行時期驗證
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number().min(0).max(150)
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<Result<User, string>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return Result.failure(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
const validatedUser = UserSchema.parse(data);
return Result.success(validatedUser);
} catch (error) {
if (error instanceof z.ZodError) {
return Result.failure(`資料驗證失敗: ${error.message}`);
}
return Result.failure("網路請求失敗");
}
}第三方套件型別問題
// 問題:第三方套件沒有型別定義或型別不完整
// 解決方案 1:自訂型別定義
// types/vendor.d.ts
declare module "some-package" {
interface SomePackageOptions {
option1: string;
option2?: number;
}
export function someFunction(options: SomePackageOptions): Promise<any>;
export default someFunction;
}
// 解決方案 2:模組擴展
declare module "existing-package" {
interface ExistingInterface {
newProperty: string;
}
}
// 解決方案 3:使用 DefinitelyTyped
npm install --save-dev @types/package-name10.4 最佳實踐檢查清單
程式碼品質檢查
型別安全性
- 避免使用
any型別 - 適當處理
null和undefined - 使用型別守衛進行執行時期驗證
- 明確定義函數回傳型別
- 避免使用
效能考量
- 避免過度複雜的型別運算
- 使用適當的型別快取策略
- 啟用增量編譯
- 排除不必要的檔案
可維護性
- 一致的命名慣例
- 適當的程式碼組織
- 完整的型別定義
- 清楚的介面設計
團隊協作檢查
工具設定
- ESLint 和 Prettier 配置
- Git hooks 設定
- VS Code 設定同步
- 統一的 tsconfig.json
文件化
- API 文件更新
- 型別定義註解
- 範例程式碼
- 最佳實踐指南
11. TypeScript 6.0 與 7.0 新特性與遷移
背景說明: TypeScript 6.0(2026 年 2 月 Beta)是最後一個基於 JavaScript 的版本,作為從 5.9 到 7.0 的過渡橋樑。TypeScript 7.0 將是全新的原生編譯器(以 Go 語言重寫),帶來約 10 倍的編譯速度提升和多執行緒支援。
11.1 TypeScript 6.0 新特性
es2025 目標支援
TypeScript 6.0 新增 es2025 作為 target 和 lib 選項,包含新的內建 API 型別:
// tsconfig.json
{
"compilerOptions": {
"target": "es2025",
"lib": ["es2025", "dom"]
}
}
// RegExp.escape - 安全地轉義正則表達式字元
function matchWholeWord(word: string, text: string) {
const escapedWord = RegExp.escape(word);
const regex = new RegExp(`\\b${escapedWord}\\b`, "g");
return text.match(regex);
}
// 使用範例
matchWholeWord("hello (world)", "say hello (world) today");Temporal API 型別支援
TypeScript 6.0 內建了 Temporal API 的型別定義,透過 --target esnext 或 "lib": ["esnext"] 即可使用:
// Temporal API - 現代化的日期時間處理
let yesterday = Temporal.Now.instant().subtract({
hours: 24,
});
let tomorrow = Temporal.Now.instant().add({
hours: 24,
});
console.log(`昨天: ${yesterday}`);
console.log(`明天: ${tomorrow}`);
// 使用 PlainDate 處理日期
const today = Temporal.Now.plainDateISO();
const nextWeek = today.add({ days: 7 });
console.log(`下週同一天: ${nextWeek}`);
// 使用 ZonedDateTime 處理時區
const taipeiTime = Temporal.Now.zonedDateTimeISO("Asia/Taipei");
console.log(`台北時間: ${taipeiTime}`);Map 的 getOrInsert 方法
ECMAScript “upsert” 提案已達到 Stage 4,新增了 getOrInsert 和 getOrInsertComputed 方法:
// 舊的寫法
function processOptions(options: Map<string, unknown>) {
let strictValue: unknown;
if (options.has("strict")) {
strictValue = options.get("strict");
} else {
strictValue = true;
options.set("strict", strictValue);
}
}
// ✅ 使用 getOrInsert(TS 6.0 + esnext lib)
function processOptions(options: Map<string, unknown>) {
let strictValue = options.getOrInsert("strict", true);
// 如果 "strict" 已存在,返回現有值;否則設定為 true 並返回
}
// getOrInsertComputed - 延遲計算預設值
function getConfig(cache: Map<string, Config>, key: string) {
return cache.getOrInsertComputed(key, (k) => {
// 只有在 key 不存在時才會執行此計算
return loadConfigFromDisk(k);
});
}Subpath Imports 支援 #/ 前綴
Node.js 新增了以 #/ 開頭的 subpath imports 支援,TypeScript 6.0 在 node20、nodenext 和 bundler 模組解析下支援此功能:
// package.json
{
"name": "my-package",
"type": "module",
"imports": {
"#": "./dist/index.js",
"#/*": "./dist/*"
}
}// 使用 #/ 前綴取代長相對路徑
import * as utils from "#/utils.js";
// 等同於 import * as utils from "../../utils.js";this-less 函數的上下文敏感性改善
TypeScript 6.0 改善了使用方法語法但未使用 this 的函數推斷:
declare function callIt<T>(obj: {
produce: (x: number) => T;
consume: (y: T) => void;
}): void;
// TS 6.0 之前:使用方法語法時順序會影響推斷
// TS 6.0:如果函數未使用 this,不再被視為上下文敏感,推斷更可靠
callIt({
consume(y) { return y.toFixed(); }, // ✅ TS 6.0 正確推斷 y 為 number
produce(x: number) { return x * 2; },
});DOM lib 整合 dom.iterable
TypeScript 6.0 將 dom.iterable 和 dom.asynciterable 的內容併入 dom:
// TS 6.0 之前需要: "lib": ["dom", "dom.iterable"]
// TS 6.0 只需要: "lib": ["dom"]
for (const element of document.querySelectorAll("div")) {
console.log(element.textContent);
}11.2 TypeScript 6.0 重大變更與棄用
注意: 這些棄用可在 TS 6.0 中透過設定
"ignoreDeprecations": "6.0"暫時忽略,但 TypeScript 7.0 將完全移除這些選項。
預設值變更一覽
| 選項 | TS 5.9 預設 | TS 6.0 預設 | 說明 |
|---|---|---|---|
strict | false | true | 嚴格模式預設開啟 |
target | es5 | es2025 | 對應最新 ES 標準 |
module | commonjs | esnext | ESM 成為主流 |
types | ["*"](自動列舉) | [] | 需明確指定 |
rootDir | 推斷 | . | 預設為 tsconfig.json 目錄 |
noUncheckedSideEffectImports | false | true | 檢查副作用匯入 |
libReplacement | true | false | 改善效能 |
已棄用的選項
// ❌ 以下選項在 TS 6.0 中已棄用,TS 7.0 將移除
// 1. target: es5 - ES5 目標已棄用
// 最低支援目標為 ES2015
{
"compilerOptions": {
"target": "es5" // ❌ 棄用,改用 "es2015" 或更高
}
}
// 2. moduleResolution: node (node10) - 已棄用
{
"compilerOptions": {
"moduleResolution": "node" // ❌ 棄用,改用 "nodenext" 或 "bundler"
}
}
// 3. baseUrl - 已棄用
{
"compilerOptions": {
"baseUrl": "./src", // ❌ 棄用
"paths": {
"@app/*": ["app/*"] // ❌ 舊寫法
}
}
}
// ✅ 改為直接在 paths 中使用完整路徑
{
"compilerOptions": {
"paths": {
"@app/*": ["./src/app/*"] // ✅ 新寫法
}
}
}
// 4. module: amd / umd / systemjs - 已棄用
// 5. outFile - 已棄用(改用外部打包工具)
// 6. moduleResolution: classic - 已移除
// 7. downlevelIteration - 已棄用(因 ES5 目標已棄用)
// 8. alwaysStrict: false - 已棄用(JS 嚴格模式始終啟用)Import 語法變更
// ❌ import assertions 語法已棄用
import blob from "./data.json" assert { type: "json" }; // ❌ 錯誤
// ✅ 改用 import attributes 語法(with 關鍵字)
import blob from "./data.json" with { type: "json" }; // ✅ 正確namespace vs module 關鍵字
// ❌ 使用 module 關鍵字定義命名空間已是硬性棄用
module Foo { // ❌ 錯誤
export const bar = 10;
}
// ✅ 使用 namespace 關鍵字
namespace Foo { // ✅ 正確
export const bar = 10;
}
// ✅ 宣告模組的語法仍然完全支援
declare module "some-module" {
export function doSomething(): void;
}11.3 TypeScript 7.0 原生編譯器 (Go 版本)
TypeScript 7.0(代號 “Project Corsa”)是以 Go 語言重寫的全新編譯器,截至 2025 年 12 月已取得重大進展。
核心優勢
效能比較(相比 TS 5.9/6.0):
┌──────────────────────────────────────────┐
│ 完整建置速度 約 10x 提升 │
│ 增量建置速度 大幅提升(具體依專案) │
│ 記憶體使用 顯著降低 │
│ 多專案並行建置 ✅ 支援 │
│ 共享記憶體多執行緒 ✅ 支援 │
└──────────────────────────────────────────┘安裝與使用
# 安裝原生預覽版本
npm install -D @typescript/native-preview
# 使用 tsgo 命令(類似 tsc)
npx tsgo --noEmit
# 與 tsc 並行使用
npx tsc -b some.tsconfig.json --extendedDiagnostics
npx tsgo -b some.tsconfig.json --extendedDiagnostics
# VS Code 擴充功能
# 安裝 "TypeScript Native Preview" 擴充功能以獲得更快的編輯器體驗已完成的功能(截至 2025 年 12 月)
編譯器功能:
- 型別檢查(約 20,000 測試案例中的高度相容性)
--incremental增量編譯- 專案參考(Project References)與
--build模式 - 多專案並行建置
語言服務功能:
- 程式碼自動完成(包含自動匯入)
- 跳至定義 / 跳至型別定義 / 跳至實作
- 尋找所有參考 / 重新命名
- 快速資訊 / 懸停提示
- 簽名說明 / 格式化
- 程式碼透鏡 / 呼叫層次結構
- 缺少匯入的快速修復
已知限制
// 1. JavaScript emit 管線尚未完全完成
// - 如果使用 Babel、esbuild 等外部工具轉譯可正常使用
// - 原生編譯目標目前最低支援到 es2021
// - 尚不支援編譯舊版裝飾器
// 2. --watch 模式可能在某些情境較不高效
// 替代方案:使用 nodemon + tsgo --incremental
// 3. 不支援 TS 5.x/6.0 的 JavaScript API
// - 依賴 TypeScript API 的工具(如 linter)暫不支援
// - 可同時安裝 typescript 和 @typescript/native-preview 並行使用
// 4. JavaScript 型別檢查(JSDoc)有差異
// - 不再識別 @enum 和 @constructor 標籤
// - Object 不再被解讀為 any
// - String 不再被解讀為 string使用標準 LSP 協定
// TypeScript 7.0 使用標準 LSP 協定取代自訂 TSServer 協定
// 這意味著任何支援 LSP 的編輯器都能獲得更好的 TypeScript 支援
// VS Code 原生預覽擴充功能可在 VS Code Marketplace 取得
// 擴充功能名稱:TypeScript Native Preview
// 發行者:TypeScriptTeam11.4 從 5.x 遷移至 6.0/7.0 指南
步驟 1:評估現有專案
# 使用 ts5to6 工具自動調整 tsconfig.json
npx @andrewbranch/ts5to6 --fixBaseUrl your-tsconfig.json
npx @andrewbranch/ts5to6 --fixRootDir your-tsconfig.json步驟 2:更新 tsconfig.json
// 遷移前(TS 5.x 典型配置)
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"baseUrl": "./src",
"paths": {
"@app/*": ["app/*"],
"@lib/*": ["lib/*"]
},
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
// 遷移後(TS 6.0 相容配置)
{
"compilerOptions": {
"target": "ES2025",
"module": "ESNext",
"moduleResolution": "bundler",
"rootDir": "./src",
"paths": {
"@app/*": ["./src/app/*"],
"@lib/*": ["./src/lib/*"]
},
"strict": true,
"types": ["node"],
"esModuleInterop": true,
"noUncheckedSideEffectImports": true
}
}步驟 3:修正常見遷移問題
// 問題 1:types 預設為 [] 導致找不到全域型別
// 錯誤: Cannot find name 'process' / 'describe' / 'it'
// 解決: 在 tsconfig.json 中明確指定
{
"compilerOptions": {
"types": ["node", "jest"] // 根據專案需要添加
}
}
// 問題 2:rootDir 預設為 . 導致輸出路徑錯誤
// 輸出到 dist/src/index.js 而非 dist/index.js
// 解決: 明確設定 rootDir
{
"compilerOptions": {
"rootDir": "./src"
}
}
// 問題 3:import assertions 語法錯誤
// ❌ 舊語法
import data from "./config.json" assert { type: "json" };
// ✅ 新語法
import data from "./config.json" with { type: "json" };
// 問題 4:namespace 使用 module 關鍵字
// ❌ 舊語法
module MyApp { export const version = "1.0"; }
// ✅ 新語法
namespace MyApp { export const version = "1.0"; }步驟 4:嘗試 TypeScript 7.0 原生預覽
# 1. 安裝原生預覽版
npm install -D @typescript/native-preview
# 2. 並行使用進行型別檢查
npx tsgo --noEmit
# 3. 比較結果
npx tsc --noEmit # TS 5.9/6.0 結果
npx tsgo --noEmit # TS 7.0 結果
# 4. 使用 --stableTypeOrdering 比較差異
# (僅用於 6.0 到 7.0 遷移診斷,不建議長期使用)
npx tsc --stableTypeOrdering --noEmit遷移檢查清單
tsconfig.json 更新
- 移除
baseUrl,在paths中使用完整路徑 - 設定明確的
rootDir - 設定明確的
types陣列 - 將
moduleResolution: node改為nodenext或bundler - 確認
target不是es5 - 確認
module不是amd/umd/systemjs
- 移除
程式碼更新
- 將
module命名空間語法改為namespace - 將
import ... assert改為import ... with - 檢查
esModuleInterop相關的匯入語法
- 將
測試驗證
- 執行
tsc --noEmit確認無編譯錯誤 - 嘗試
tsgo --noEmit驗證 7.0 相容性 - 運行完整測試套件確認功能正常
- 執行
工具鏈檢查
- 確認使用的建置工具支援新語法
- 確認 ESLint/Prettier 設定與 TS 6.0 相容
- 安裝 VS Code 原生預覽擴充功能進行體驗評估
本章重點回顧:
- TypeScript 6.0 是過渡版本,引入大量預設值變更和棄用項目
- 多個長期使用的選項已棄用:
baseUrl、target: es5、moduleResolution: node等 - TypeScript 7.0 以 Go 語言重寫,帶來約 10 倍編譯速度提升
- 使用
ts5to6工具和@typescript/native-preview逐步準備遷移 - 新增 Temporal API、
RegExp.escape、Map.getOrInsert等現代 API 型別支援
總結
這份完整的 TypeScript 教學手冊涵蓋了從基礎概念到進階應用的所有重要主題。透過系統性的學習和實踐,開發者能夠充分掌握 TypeScript 的強大功能,提升程式碼品質和開發效率。
記住,TypeScript 的核心價值在於型別安全性和開發體驗的提升。持續學習新特性,關注社群最佳實踐,並在專案中逐步應用這些知識,將有助於成為一名優秀的 TypeScript 開發者。
📚 延伸學習資源
官方文件
- TypeScript 官方手冊
- TypeScript 遊樂場
- TypeScript 6.0 Beta 公告
- TypeScript 7.0 進度報告
- TypeScript Go 原生編譯器 (GitHub)
- @typescript/native-preview (npm)
社群資源
工具和函式庫
文件資訊
版本:v3.0
最後更新:2026年2月20日
作者:開發團隊