TypeScript 編程最佳實踐

TypeScript 編程最佳實踐

本文介紹 TypeScript 編程的最佳實踐。

本指南說明如何運用 TypeScript 的型別來減少錯誤並寫出更易讀的程式碼的實用最佳實踐。

YouTube Video

TypeScript 編程最佳實踐

TypeScript 最大的優點是「透過型別來防止錯誤並使程式碼意圖明確」。

最佳實踐不僅僅是規則,而是用於撰寫安全、可讀且易於維護程式碼的一套原則。以下將結合實際範例,介紹常見的 TypeScript 最佳實踐。

避免使用 any,並盡量賦予型別有意義的定義

首先,讓我們來看看“避免使用 any 並賦予有意義的型別”這一點。

any 會完全停用型別檢查,這會讓使用 TypeScript 的意義喪失。不要為了讓程式暫時執行而使用 any,而應該盡可能詳細定義型別

1// Bad
2function parse(data: any) {
3  return data.value;
4}

這段程式碼可以接受任何值,因此無法防止執行時錯誤。

1// Good
2type ParsedData = {
3  value: string;
4};
5
6function parse(data: ParsedData): string {
7  return data.value;
8}

透過定義型別,可以明確表示輸入和輸出的意圖,並提升安全性。

請務必使用 typeinterface 明確定義物件結構。

接下來,讓我們來看看“始終使用 typeinterface 明確定義物件結構”這一點。

若臨時使用物件,其結構容易變得模糊不清。請務必將型別拆出以提升重用性與維護性。

1// Bad
2function createUser(user: { name: string; age: number }) {
3  console.log(user.name);
4}

即使是在小段程式碼中,也要養成分離型別的習慣是很重要的。

1// Good
2type User = {
3  name: string;
4  age: number;
5};
6
7function createUser(user: User): void {
8  console.log(user.name);
9}

為型別命名可以讓整體程式碼的理解更加容易。

利用聯合型別精確表達所有可能的狀態。

如果用原始的 stringnumber 型別作為條件,容易出現未預期的值。透過使用聯合型別,你可以在型別層級上只表示允許的狀態。

1// Bad
2function setStatus(status: string) {
3  console.log(status);
4}

在這段程式碼中,無意義的字串或錯誤的值無法在編譯階段被檢測出來。

1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4function setStatus(status: Status): void {
5  console.log(status);
6}

透過使用聯合型別,你可以在編譯階段可靠地消除「不可能的狀態」。因此,條件分支的安全性與程式碼的可靠性都會提升。

請明確處理 nullundefined

然後,讓我們來看看“明確處理 nullundefined”這一點。

在 TypeScript 中,用型別表示值可能不存在是很重要的。如果保持模糊不明,容易導致執行時錯誤。

1type User = {
2  name: string;
3  email?: string;
4};

email 有可能不存在,因此應以此為前提處理。

1function printEmail(user: User): void {
2  if (user.email) {
3    console.log(user.email);
4  }
5}

在使用可選值前務必進行檢查。

僅在不得已時才使用型別斷言 (as)。

再來,讓我們來看看“不過度使用型別斷言”這一點。

型別斷言會暫時略過 TypeScript 的型別檢查,來宣告「我知道這個值是這個型別」。過度使用會破壞型別安全性。

1// Bad
2const value = input as string;

在這段程式碼中,即使實際值不是字串,也不會出現錯誤,這可能會導致執行階段錯誤。如下列程式碼所示,請優先選擇使用型別保護進行安全檢查。

1// Good
2function isString(value: unknown): value is string {
3  return typeof value === "string";
4}
5
6if (isString(input)) {
7  console.log(input.toUpperCase());
8}

型別保護是一種能在檢查實際值時安全地判斷型別的機制。將型別保護優先於型別斷言,可以更容易地防止執行階段錯誤。

請勿過度依賴回傳值型別的型別推論。

接著,讓我們來看看“不過度依賴型別推斷來決定回傳型別”這一點。

雖然 TypeScript 的型別推論很強大,但對公開函式,明確標註回傳型別更安全。這樣可以將未來變更造成的影響降到最低。

1// Bad
2function sum(a: number, b: number) {
3  return a + b;
4}

即使是小函式,也要明確表達用途與意圖。

1// Good
2function sum(a: number, b: number): number {
3  return a + b;
4}

明確標示回傳型別能提升 API 的穩定性。

請用 unknown 安全處理輸入。

此外,讓我們來看看“使用 unknown 安全地接收外部輸入”這一點。

對於如 API、JSON 或使用者輸入等外部輸入,請使用 unknown 而非 any。這麼做可以確保所有值都經過驗證,維持型別安全。

1// Bad
2function handleResponse(data: any) {
3  console.log(data.id);
4}

以下是如何利用 unknown 驗證型別。

 1// Good
 2function handleResponse(data: unknown): void {
 3  if (
 4    typeof data === "object" &&
 5    data !== null &&
 6    "id" in data
 7  ) {
 8    console.log((data as { id: number }).id);
 9  }
10}

unknown 不能直接使用;這是一種需要驗證的型別。在處理外部輸入時特別有效。

透過組合小型型別提升表達能力。

還有,讓我們來看看“透過組合小型別來提升表現力”這一點。

一次定義大型型別會降低可讀性與可維護性。請將型別拆分為有意義的小單元,並在需要時組合。

 1type Id = number;
 2
 3type UserProfile = {
 4  id: Id;
 5  name: string;
 6};
 7
 8type UserWithStatus = UserProfile & {
 9  status: "active" | "inactive";
10};

將型別當作元件般組合有助於結構化設計。

typeinterface

interface 的優點

typeinterface 雖然都能定義型別,但用途與特性有所不同。根據用途區分使用,可以使型別定義更容易傳達意圖。

 1// Bad
 2type User = {
 3  id: number;
 4  name: string;
 5};
 6
 7type AdminUser = {
 8  id: number;
 9  name: string;
10  role: "admin";
11};

像這樣重複共通部分,會讓程式碼容易對變更產生脆弱性。

1// Good
2interface User {
3  id: number;
4  name: string;
5}
6
7interface AdminUser extends User {
8  role: "admin";
9}

interface 適合需要擴充(extends)的設計,也適用於表達物件「形狀」。

type 的優點

另一方面,type 更具有表達力,適合用於聯合型別與交叉型別。

1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4type ApiResponse<T> =
5  | { status: "success"; data: T }
6  | { status: "error"; message: string };

type 很適合用於表達狀態、選項或各種組合。

選擇 typeinterface 的指引

一般建議:物件結構與契約請使用 interface,有聯合、交叉、型別運算等需求時請使用 type

兩者有許多相似之處,但選用時仍應考量哪一種型別更能清楚傳達其存在理由。

請將型別視為文件。

最後,讓我們來看看“將型別作為文件來書寫”這一點。

良好的型別定義能傳達比註解更充分的資訊。重要的是要以“僅透過型別就能理解規格”的狀態為目標。

1type ApiError = {
2  code: number;
3  message: string;
4  retryable: boolean;
5};

如此一來,TypeScript 的一大優勢就是型別定義能夠作為一種規格文件來發揮作用。

總結

TypeScript 的最佳實踐重點並非過度嚴苛。本質在於透過型別明確表達意圖,並寫出容易因應變動的程式碼

在日常開發累積這些小原則,長期下來就能達成「更容易審查」、「錯誤更少」、「讓自己與他人未來更好理解程式碼」等目標。

請先思考「這樣可以用型別怎麼表達?」,你就能寫出高品質、充分發揮 TypeScript 優勢的程式碼。

您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。

YouTube Video