Skip to content

JavaScript/TypeScript 最佳實踐

本文件定義 ITO 前端團隊的 JavaScript 與 TypeScript 開發規範。假設你已熟悉 JavaScript/TypeScript 基礎語法,這裡說明的是團隊在多種合理做法中的選擇,而非基礎語法教學。

函式定義與流程控制

JS - 偏好使用 function 宣告具名函式

具名函式使用 function 關鍵字宣告,僅在 callback、匿名函數或需要簡潔語法的場景使用箭頭函式。

原因

  • function 關鍵字語意明確,一眼就能辨識是函式定義
  • 具名函式的宣告提升特性在某些情境更方便
  • 箭頭函式適合簡短的 callback,讓程式碼更簡潔

✅ Good

js
function traditionalFunction() {
  // ...
}

const doubleArr = [1, 2, 3].map(val => val * 2);

❌ Bad

js
// 具名函式不應使用 arrow function
const arrowFunction = () => {
  // ...
};

// callback 不應使用傳統函式語法
const tripleArr = [1, 2, 3].map(function (val) {
  return val * 3;
});

JS - 參數超過三個時改用物件傳遞

當函式參數超過三個時,改用物件型別傳遞參數,方便擴充與維護。

原因

  • 呼叫端不需要記住參數順序,降低出錯機率
  • 參數更加結構化,語意更清楚
  • 容易擴充,新增參數不影響現有呼叫
  • 支援選擇性參數,不會有漏傳問題

✅ Good

js
function createUser({ nameageemailrole }) {
  // ...
}

createUser({
  name: '小明',
  age: 25,
  email: 'ming@example.com',
  role: 'admin',
});

❌ Bad

js
function createUser(name, age, email, role) {
  // ...
}

createUser('小明', 25, 'ming@example.com', 'admin');

JS - 避免過深的巢狀判斷

優先處理邊界條件與錯誤情況,提早 return,避免深層巢狀的 if-else 結構。

原因

  • 提高程式碼可讀性,主要邏輯更清晰
  • 減少巢狀結構,降低認知複雜度
  • 錯誤處理集中在函式開頭,容易維護

✅ Good

js
function validateUser(user) {
  if (!user) return false;
  if (!user.email) return false;
  if (!user.age || user.age < 18) return false;

  return true;
}

❌ Bad

js
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-elseswitch

原因

  • 程式碼更簡潔,邏輯更清晰
  • 易於維護與擴充,新增條件只需在物件中新增一筆資料
  • 效能通常更好,O(1) > O(n)

✅ Good

js
const STATUS_COLORS = {
  pending: 'yellow',
  success: 'green',
  fail: 'red',
  unknown: 'gray',
};

function getStatusColor(status) {
  return STATUS_COLORS[status] || STATUS_COLORS.unknown;
}

❌ Bad

js
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 方法(如 mapfilterreducetoSorted)處理資料,避免直接修改原陣列。

原因

  • 語意更清晰,程式碼更易讀
  • 不會修改原始陣列,避免副作用,更容易 debug

✅ Good

js
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

js
const numbers = [3, 1, 2];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
  doubled.push(numbers[i] * 2);
}
numbers.sort(); // 會修改原陣列

程式碼品質

JS - 變數命名規則

遵循統一的命名慣例,讓變數名稱傳達明確的語意與型別資訊。

原因

  • 統一的命名規則提升程式碼可讀性
  • 變數名稱傳達型別資訊,減少認知負擔
  • 易於搜尋與重構

規則

  • boolean 變數:以 ishasneedshould 等開頭
  • array 變數:以 ArrList 結尾或使用英文單字複數形

✅ Good

js
const isActive = true;
const hasPermission = false;
const shouldUpdate = true;

const userList = [];
const itemsArr = [];
const products = []; // 複數形

❌ Bad

js
const active = true; // boolean 不明確
const permission = false;

const user = []; // 看起來像單一物件,實際是陣列
const item = [];

JS - 避免魔術數字與字串

將重複使用或有特定意義的數字/字串定義為常數,提升程式碼可讀性與維護性。

原因

  • 賦予數字/字串明確的語意,程式碼更易理解
  • 集中管理,修改時只需改一處
  • 避免打錯字或數值不一致的問題

✅ Good

js
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

js
function fetchWithRetry(url) {
  let retries = 0;
  while (retries < 3) {
    // 3 代表什麼意義?
    // ...
  }
}

setTimeout(callback, 5000); // 5000 是多久?

非同步與錯誤處理

JS - 錯誤處理

明確處理錯誤,避免靜默失敗或讓錯誤擴散到不可預期的地方。

原因

  • 避免靜默失敗,錯誤訊息有助於 debug
  • 在合適的層級處理錯誤,讓程式流程更清晰
  • 提供有意義的錯誤訊息,方便追蹤問題

✅ Good

js
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

js
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

js
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

js
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

ts
type Person = {
  name: string;
  age: number;
};

type Women = Person & { gender: 'F' };

type Status = 'active' | 'inactive';

❌ Bad

ts
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

ts
const STATUS_MAP = {
  '-1': 'fail',
  1: 'success',
  0: 'pending',
} as const;

type Status = (typeof STATUS_MAP)[keyof typeof STATUS_MAP]; // 'fail' | 'success' | 'pending'

❌ Bad

ts
enum Status {
  Fail = -1,
  Success = 1,
  Pending = 0,
}

TS - 善用 Utility Types

使用 TypeScript 內建的 Utility Types 處理型別轉換,避免重複定義。

原因

  • 提高程式碼可維護性,避免重複定義型別
  • TypeScript 內建許多實用的 Utility Types,涵蓋常見需求
  • 型別轉換更語意化,易於理解

✅ Good

ts
type User = {
  id: number;
  name: string;
  email: string;
  password: string;
};

type PublicUser = Omit<User'password'>;
type UserUpdatePayload = Partial<User>;
type UserKeys = keyof User;

❌ Bad

ts
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 會讓編譯器跳過型別檢查,等於沒寫 TypeScript
  • unknown 代表不確定的型別,強制你在使用前進行型別檢查
  • 提升程式碼安全性,避免 runtime 錯誤

✅ Good

ts
function processData(data: unknown) {
  if (typeof data === 'string') {
    return data.toUpperCase();
  }
  if (typeof data === 'number') {
    return data * 2;
  }
  throw new Error('Unsupported type');
}

❌ Bad

ts
function processData(data: any) {
  return data.toUpperCase(); // 可能在 runtime 出錯,但 TypeScript 不會警告
}

TS - 避免濫用 as

使用 Type guard 進行型別檢查,避免濫用 as 跳過 TypeScript 的型別檢查。

原因

  • as 會強制轉換型別,跳過編譯器檢查,可能導致 runtime 錯誤
  • Type guard 提供真正的型別檢查,更安全
  • 讓 TypeScript 的型別推斷發揮作用

✅ Good

ts
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

ts
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

ts
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

ts
function sendEmail(user: User | null) {
  emailService.send(user!.email!); // 可能 runtime 錯誤
}

function getUserName(user: User | null): string {
  return user!.name; // 假設 user 一定存在
}

TS - 定義精確的型別

盡可能使用精確的型別,避免過於寬鬆的型別定義。

原因

  • 精確的型別提供更好的型別檢查,在編譯期就能發現錯誤
  • IDE 自動補全更準確,開發體驗更好
  • 程式碼意圖更明確,易於維護

✅ Good

ts
type Status = 'pending' | 'error' | 'success';

function checkStatus(status: Status) {
  // TypeScript 會檢查 status 只能是這三個值
}

❌ Bad

ts
function checkStatus(status: string) {
  // 任何字串都可以傳入,失去型別保護
}

進階型別技巧

TS - 優先使用 satisfies

使用 satisfies 進行型別檢查,同時保留物件的完整型別推斷。

原因

  • satisfies 確保物件符合型別約束,同時保留額外屬性的型別資訊
  • 比起型別標註(:)更靈活,不會丟失型別推斷
  • 在需要型別檢查但又想保留完整型別資訊時非常有用

✅ Good

ts
const axis = {
  x: 1,
  y: 2,
  extra: 3,
} satisfies { x: number; y: number };

// axis.extra 仍可存取,型別為 number

❌ Bad

ts
const axis: { x: number; y: number } = {
  x: 1,
  y: 2,
  extra: 3, // ❌ 型別錯誤
};

TS - 善用泛型並以 extends 限制型別

使用泛型(Generics)增加函式的靈活性,並使用 extends 關鍵字約束泛型範圍,確保型別安全。

原因

  • 提高程式碼複用性,同一函式可適用於多種符合條件的型別
  • extends 確保傳入的參數擁有特定屬性或符合特定結構,提供更精確的型別檢查
  • 避免使用 any,保持型別推斷能力

✅ Good

ts
// 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

ts
// 使用 any 失去型別保護
function getId(item: any) {
  return item.id;
}

// 或者過於具體,無法複用
function getUserId(user: { id: number; name: string }) {
  return user.id;
}

函式設計

TS - 保持 Utility Function 純粹

Utility function 應保持純函式特性,不產生副作用。

原因

  • 純函式更容易測試,輸入相同輸出必定相同
  • 不依賴外部狀態,邏輯更清晰
  • 易於複用與組合

✅ Good

ts
// 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

ts
// 依賴外部狀態,有副作用
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

ts
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

ts
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

ts
import type { User, Post, Comment } from './types';
import { fetchUser, fetchPosts } from './api';

function displayUser(user: User) {
  // ...
}

❌ Bad

ts
import { User, Post, Comment, fetchUser, fetchPosts } from './api';
// User, Post, Comment 只用於型別標註,卻可能被編譯進 bundle

function displayUser(user: User) {
  // ...
}