JavaScript/TypeScript 最佳實踐
本文件定義 ITO 前端團隊的 JavaScript 與 TypeScript 開發規範。假設你已熟悉 JavaScript/TypeScript 基礎語法,這裡說明的是團隊在多種合理做法中的選擇,而非基礎語法教學。
函式定義與流程控制
JS - 偏好使用 function 宣告具名函式
具名函式使用 function 關鍵字宣告,僅在 callback、匿名函數或需要簡潔語法的場景使用箭頭函式。
原因
function關鍵字語意明確,一眼就能辨識是函式定義- 具名函式的宣告提升特性在某些情境更方便
- 箭頭函式適合簡短的 callback,讓程式碼更簡潔
✅ Good
function traditionalFunction() {
// ...
}
const doubleArr = [1, 2, 3].map(val => val * 2);❌ Bad
// 具名函式不應使用 arrow function
const arrowFunction = () => {
// ...
};
// callback 不應使用傳統函式語法
const tripleArr = [1, 2, 3].map(function (val) {
return val * 3;
});JS - 參數超過三個時改用物件傳遞
當函式參數超過三個時,改用物件型別傳遞參數,方便擴充與維護。
原因
- 呼叫端不需要記住參數順序,降低出錯機率
- 參數更加結構化,語意更清楚
- 容易擴充,新增參數不影響現有呼叫
- 支援選擇性參數,不會有漏傳問題
✅ Good
function createUser({ name, age, email, role }) {
// ...
}
createUser({
name: '小明',
age: 25,
email: 'ming@example.com',
role: 'admin',
});❌ Bad
function createUser(name, age, email, role) {
// ...
}
createUser('小明', 25, 'ming@example.com', 'admin');JS - 避免過深的巢狀判斷
優先處理邊界條件與錯誤情況,提早 return,避免深層巢狀的 if-else 結構。
原因
- 提高程式碼可讀性,主要邏輯更清晰
- 減少巢狀結構,降低認知複雜度
- 錯誤處理集中在函式開頭,容易維護
✅ Good
function validateUser(user) {
if (!user) return false;
if (!user.email) return false;
if (!user.age || user.age < 18) return false;
return true;
}❌ Bad
function validateUser(user) {
if (user) {
if (user.email) {
if (user.age && user.age >= 18) {
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
}JS - 使用搜索代替分支
當有多個條件對應不同值的狀況時,優先使用 Object 或 Map 的查找(Lookup)方式,取代大量的 if-else 或 switch。
原因
- 程式碼更簡潔,邏輯更清晰
- 易於維護與擴充,新增條件只需在物件中新增一筆資料
- 效能通常更好,O(1) > O(n)
✅ Good
const STATUS_COLORS = {
pending: 'yellow',
success: 'green',
fail: 'red',
unknown: 'gray',
};
function getStatusColor(status) {
return STATUS_COLORS[status] || STATUS_COLORS.unknown;
}❌ Bad
function getStatusColor(status) {
if (status === 'pending') {
return 'yellow';
} else if (status === 'success') {
return 'green';
} else if (status === 'fail') {
return 'red';
} else {
return 'gray';
}
}資料處理
JS - 優先使用 Immutable 陣列方法
使用陣列的 immutable 方法(如 map、filter、reduce、toSorted)處理資料,避免直接修改原陣列。
原因
- 語意更清晰,程式碼更易讀
- 不會修改原始陣列,避免副作用,更容易 debug
✅ Good
const numbers = [3, 1, 2];
const doubled = numbers.map(val => val * 2);
const evens = numbers.filter(val => val % 2 === 0);
const sorted = numbers.toSorted();❌ Bad
const numbers = [3, 1, 2];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
numbers.sort(); // 會修改原陣列程式碼品質
JS - 變數命名規則
遵循統一的命名慣例,讓變數名稱傳達明確的語意與型別資訊。
原因
- 統一的命名規則提升程式碼可讀性
- 變數名稱傳達型別資訊,減少認知負擔
- 易於搜尋與重構
規則
- boolean 變數:以
is、has、need、should等開頭 - array 變數:以
Arr、List結尾或使用英文單字複數形
✅ Good
const isActive = true;
const hasPermission = false;
const shouldUpdate = true;
const userList = [];
const itemsArr = [];
const products = []; // 複數形❌ Bad
const active = true; // boolean 不明確
const permission = false;
const user = []; // 看起來像單一物件,實際是陣列
const item = [];JS - 避免魔術數字與字串
將重複使用或有特定意義的數字/字串定義為常數,提升程式碼可讀性與維護性。
原因
- 賦予數字/字串明確的語意,程式碼更易理解
- 集中管理,修改時只需改一處
- 避免打錯字或數值不一致的問題
✅ Good
const MAX_RETRY_COUNT = 3;
const API_TIMEOUT = 5000;
const DEFAULT_PAGE_SIZE = 20;
function fetchWithRetry(url) {
let retries = 0;
while (retries < MAX_RETRY_COUNT) {
// ...
}
}
setTimeout(callback, API_TIMEOUT);❌ Bad
function fetchWithRetry(url) {
let retries = 0;
while (retries < 3) {
// 3 代表什麼意義?
// ...
}
}
setTimeout(callback, 5000); // 5000 是多久?非同步與錯誤處理
JS - 錯誤處理
明確處理錯誤,避免靜默失敗或讓錯誤擴散到不可預期的地方。
原因
- 避免靜默失敗,錯誤訊息有助於 debug
- 在合適的層級處理錯誤,讓程式流程更清晰
- 提供有意義的錯誤訊息,方便追蹤問題
✅ Good
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error fetching user:', error);
throw error; // 重新拋出讓上層處理
}
}❌ Bad
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
return await response.json();
} catch (error) {
// 什麼都不做,靜默失敗
}
}JS - 優先使用 Async/Await
使用 async/await 語法處理非同步操作,避免 Promise chain 造成的巢狀結構。
原因
async/await讓非同步程式碼看起來像同步程式碼,更易閱讀- 錯誤處理更直觀,可以使用
try/catch - 避免 Promise chain 的巢狀結構
✅ Good
async function loadUserData(userId) {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts.map(p => p.id));
return { user, posts, comments };
}❌ Bad
function loadUserData(userId) {
return fetchUser(userId)
.then(user => {
return fetchPosts(user.id).then(posts => {
return fetchComments(posts.map(p => p.id)).then(comments => {
return { user, posts, comments };
});
});
})
.catch(error => {
console.error(error);
});
}型別定義
TS - 一律使用 Type 定義型別
統一使用 type 定義所有型別,包括物件型別,避免使用 interface。
原因
interface在同名情況下會自動合併,可能造成非預期結果type同時適用於原始型別與物件型別,interface僅適用物件型別,統一使用type降低心智負擔type語法更簡潔
✅ Good
type Person = {
name: string;
age: number;
};
type Women = Person & { gender: 'F' };
type Status = 'active' | 'inactive';❌ Bad
interface IPerson {
name: string;
age: number;
}
interface Men extends IPerson {
gender: 'M';
}TS - 禁止使用 Enum
避免使用 TypeScript 的 Enum,改用 as const 定義常數物件。
原因
- JavaScript 原生不支援
Enum,編譯後會產生許多冗餘程式碼 Enum有許多不可預期的行為(如反向對應、數字列舉自動遞增等)as const更貼近 JavaScript 原生語法,編譯後程式碼更簡潔
✅ Good
const STATUS_MAP = {
'-1': 'fail',
1: 'success',
0: 'pending',
} as const;
type Status = (typeof STATUS_MAP)[keyof typeof STATUS_MAP]; // 'fail' | 'success' | 'pending'❌ Bad
enum Status {
Fail = -1,
Success = 1,
Pending = 0,
}TS - 善用 Utility Types
使用 TypeScript 內建的 Utility Types 處理型別轉換,避免重複定義。
原因
- 提高程式碼可維護性,避免重複定義型別
- TypeScript 內建許多實用的 Utility Types,涵蓋常見需求
- 型別轉換更語意化,易於理解
✅ Good
type User = {
id: number;
name: string;
email: string;
password: string;
};
type PublicUser = Omit<User, 'password'>;
type UserUpdatePayload = Partial<User>;
type UserKeys = keyof User;❌ Bad
type User = {
id: number;
name: string;
email: string;
password: string;
};
// 重複定義型別
type PublicUser = {
id: number;
name: string;
email: string;
};
type UserUpdatePayload = {
id?: number;
name?: string;
email?: string;
password?: string;
};型別安全
TS - 避免使用 any
避免使用 any 跳過型別檢查,改用 unknown 保持型別安全。
原因
any會讓編譯器跳過型別檢查,等於沒寫 TypeScriptunknown代表不確定的型別,強制你在使用前進行型別檢查- 提升程式碼安全性,避免 runtime 錯誤
✅ Good
function processData(data: unknown) {
if (typeof data === 'string') {
return data.toUpperCase();
}
if (typeof data === 'number') {
return data * 2;
}
throw new Error('Unsupported type');
}❌ Bad
function processData(data: any) {
return data.toUpperCase(); // 可能在 runtime 出錯,但 TypeScript 不會警告
}TS - 避免濫用 as
使用 Type guard 進行型別檢查,避免濫用 as 跳過 TypeScript 的型別檢查。
原因
as會強制轉換型別,跳過編譯器檢查,可能導致 runtime 錯誤Type guard提供真正的型別檢查,更安全- 讓 TypeScript 的型別推斷發揮作用
✅ Good
function isUser(data: unknown): data is User {
return (
typeof data === 'object' && data !== null && 'id' in data && 'name' in data
);
}
function processData(data: unknown) {
if (isUser(data)) {
console.log(data.id); // TypeScript 知道這是 User
console.log(data.name);
}
}❌ Bad
function processData(data: unknown) {
const user = data as User; // 跳過型別檢查,可能 runtime 錯誤
console.log(user.id);
console.log(user.name);
}TS - 避免使用 !
明確檢查 null/undefined,避免使用 ! 運算子跳過檢查。
原因
!告訴編譯器「這個值一定不是 null/undefined」,但無法保證 runtime 確實如此- 明確檢查更安全,程式碼意圖更清楚
- 避免 runtime 錯誤
✅ Good
function sendEmail(user: User | null) {
if (user?.email) {
// 明確檢查
emailService.send(user.email);
} else {
console.error('User email not found');
}
}
// 或使用 optional chaining
function getUserName(user: User | null): string {
return user?.name ?? 'Guest';
}❌ Bad
function sendEmail(user: User | null) {
emailService.send(user!.email!); // 可能 runtime 錯誤
}
function getUserName(user: User | null): string {
return user!.name; // 假設 user 一定存在
}TS - 定義精確的型別
盡可能使用精確的型別,避免過於寬鬆的型別定義。
原因
- 精確的型別提供更好的型別檢查,在編譯期就能發現錯誤
- IDE 自動補全更準確,開發體驗更好
- 程式碼意圖更明確,易於維護
✅ Good
type Status = 'pending' | 'error' | 'success';
function checkStatus(status: Status) {
// TypeScript 會檢查 status 只能是這三個值
}❌ Bad
function checkStatus(status: string) {
// 任何字串都可以傳入,失去型別保護
}進階型別技巧
TS - 優先使用 satisfies
使用 satisfies 進行型別檢查,同時保留物件的完整型別推斷。
原因
satisfies確保物件符合型別約束,同時保留額外屬性的型別資訊- 比起型別標註(
:)更靈活,不會丟失型別推斷 - 在需要型別檢查但又想保留完整型別資訊時非常有用
✅ Good
const axis = {
x: 1,
y: 2,
extra: 3,
} satisfies { x: number; y: number };
// axis.extra 仍可存取,型別為 number❌ Bad
const axis: { x: number; y: number } = {
x: 1,
y: 2,
extra: 3, // ❌ 型別錯誤
};TS - 善用泛型並以 extends 限制型別
使用泛型(Generics)增加函式的靈活性,並使用 extends 關鍵字約束泛型範圍,確保型別安全。
原因
- 提高程式碼複用性,同一函式可適用於多種符合條件的型別
extends確保傳入的參數擁有特定屬性或符合特定結構,提供更精確的型別檢查- 避免使用
any,保持型別推斷能力
✅ Good
// T 必須是包含 id 屬性的物件
function getId<T extends { id: number }>(item: T): number {
return item.id;
}
const user = { id: 1, name: 'Alice' };
const post = { id: 101, title: 'Hello' };
getId(user); // OK
getId(post); // OK❌ Bad
// 使用 any 失去型別保護
function getId(item: any) {
return item.id;
}
// 或者過於具體,無法複用
function getUserId(user: { id: number; name: string }) {
return user.id;
}函式設計
TS - 保持 Utility Function 純粹
Utility function 應保持純函式特性,不產生副作用。
原因
- 純函式更容易測試,輸入相同輸出必定相同
- 不依賴外部狀態,邏輯更清晰
- 易於複用與組合
✅ Good
// Pure function
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
function formatCurrency(value: number): string {
return new Intl.NumberFormat('zh-TW', {
style: 'currency',
currency: 'TWD',
}).format(value);
}❌ Bad
// 依賴外部狀態,有副作用
let tax = 0.05;
function calculateTotal(items: Item[]): number {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
tax = subtotal > 1000 ? 0.1 : 0.05; // 修改外部變數
return subtotal * (1 + tax);
}Runtime 與模組
TS - 使用 zod 進行 Runtime 驗證
使用 zod 在 runtime 驗證外部資料,確保資料符合預期型別。
原因
- TypeScript 只做 build time 的型別檢查,無法保證 runtime 資料正確性
- 外部資料(API 回應、使用者輸入等)需要在 runtime 驗證
zod提供型別推斷,避免重複定義 TypeScript 型別與驗證邏輯
✅ Good
import { z } from 'zod';
const UserSchema = z.object({
id: z.int(),
name: z.string(),
email: z.email(),
age: z.int().min(0).max(99),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return UserSchema.parse(data); // 會在 runtime 驗證資料格式
}❌ Bad
type User = {
id: number;
name: string;
email: string;
age: number;
};
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return await response.json(); // 沒有驗證,可能收到不符合型別的資料
}TS - 使用 import type 匯入型別
使用 import type 匯入只在型別層面使用的型別,避免將型別編譯進 bundle。
原因
- 明確區分型別與值的匯入,程式碼意圖更清楚
- 型別只在編譯期存在,使用
import type確保不會被編譯進最終 bundle - 減少 bundle 大小,提升效能
- 避免循環依賴問題
✅ Good
import type { User, Post, Comment } from './types';
import { fetchUser, fetchPosts } from './api';
function displayUser(user: User) {
// ...
}❌ Bad
import { User, Post, Comment, fetchUser, fetchPosts } from './api';
// User, Post, Comment 只用於型別標註,卻可能被編譯進 bundle
function displayUser(user: User) {
// ...
}