掌握 TypeScript:20 個提高代碼質量的最佳實踐

介紹

TypeScript 是一種廣泛使用的開源編程語言,非常適合現代化開發。藉助它先進的類型系統,TypeScript 允許開發者編寫更加強健、可維護和可擴展的代碼。但是,要真正發揮 TypeScript 的威力並構建高質量的項目,瞭解和遵循最佳實踐至關重要。在本文中,我們將深入探索 TypeScript 的世界,並探討掌握該語言的 21 個最佳實踐。這些最佳實踐涵蓋了各種主題,並提供瞭如何在真實項目中應用它們的具體示例。無論你是初學者還是經驗豐富的 TypeScript 開發者,本文都將提供有價值的見解和技巧,幫助你編寫乾淨高效的代碼。

最佳實踐 1:嚴格的類型檢查

我們將從最基本的實踐開始。想象一下,在問題出現之前就能發現潛在錯誤,聽起來太好不過了吧?這正是 TypeScript 中嚴格類型檢查所能爲你做到的。這個最佳實踐的目的是捕捉那些可能會悄悄溜進你的代碼並在後面引發麻煩的蟲子。

嚴格類型檢查的主要作用是確保你的變量類型與你期望的類型匹配。這意味着,如果你聲明一個變量爲字符串類型,TypeScript 將確保分配給該變量的值確實是字符串而不是數字,例如。這有助於您及早發現錯誤,並確保您的代碼按照預期工作。

啓用嚴格類型檢查只需在 tsconfig.json 文件中添加 "strict": true(默認爲 true)即可。通過這樣做,TypeScript 將啓用一組檢查,以捕獲某些本應未被發現的錯誤。

以下是一個例子,說明嚴格類型檢查如何可以幫助你避免常見錯誤:

let userName: string = "John";
userName = 123; // TypeScript will raise an error because "123" is not a string.

通過遵循這個最佳實踐,你將能夠及早發現錯誤,並確保你的代碼按照預期工作,從而爲你節省時間和不必要的麻煩。

最佳實踐 2:類型推斷

TypeScript 的核心理念是顯式地指定類型,但這並不意味着你必須在每次聲明變量時都明確指定類型。

類型推斷是 TypeScript 編譯器根據變量賦值的值自動推斷變量類型的能力。這意味着你不必在每次聲明變量時都顯式指定類型。相反,編譯器會根據值推斷類型。

例如,在以下代碼片段中,TypeScript 會自動推斷 name 變量的類型爲字符串:

let name = "John";

類型推斷在處理複雜類型或將變量初始化爲從函數返回的值時特別有用。

但是請記住,類型推斷並不是一個魔法棒,有時候最好還是顯式指定類型,特別是在處理複雜類型或確保使用特定類型時。

最佳實踐 3:使用 Linters

Linters 是一種可以通過強制一組規則和指南來幫助你編寫更好代碼的工具。它們可以幫助你捕捉潛在的錯誤,提高代碼的整體質量。

有幾個針對 TypeScript 的 Linters 可供選擇,例如 TSLint 和 ESLint,可以幫助你強制執行一致的代碼風格並捕捉潛在的錯誤。這些 Linters 可以配置檢查諸如缺少分號、未使用的變量和其他常見問題等事項。

最佳實踐 4:使用接口

當涉及到編寫乾淨、可維護的代碼時,接口是你的好朋友。它們就像是對象的藍圖,概述了你將要使用的數據的結構和屬性。

在 TypeScript 中,接口定義了對象的形狀的約定。它指定了該類型的對象應具有的屬性和方法,並且可以用作變量的類型。這意味着,當你將一個對象分配給帶有接口類型的變量時,TypeScript 會檢查對象是否具有接口中指定的所有屬性和方法。

以下是 TypeScript 中定義和使用接口的示例:

interface User {
name: string;
age: number;
}
let user: User = {name: "John", age: 25};

接口還可以使代碼重構更容易,因爲它確保了使用某個特定類型的所有位置都會被一次性更新。

最佳實踐 5:類型別名

TypeScript 允許你使用類型別名(type aliases)創建自定義類型。類型別名和接口(interface)的主要區別在於,類型別名爲類型創建一個新名稱,而接口爲對象的形狀創建一個新名稱。

例如,你可以使用類型別名爲二維空間中的點創建一個自定義類型:

type Point = { x: number, y: number };
let point: Point = { x: 0, y: 0 };

類型別名也可以用於創建複雜類型,例如聯合類型(union type)或交叉類型(intersection type)。

type User = { name: string, age: number };
type Admin = { name: string, age: number, privileges: string[] };
type SuperUser = User & Admin;

最佳實踐 6:使用元組

元組是一種表示具有不同類型的固定大小元素數組的方式。它們允許你用特定的順序和類型表示值的集合。

例如,你可以使用元組來表示二維空間中的一個點:

let point: [number, number] = [1, 2];

你還可以使用元組來表示多個類型的集合:

let user: [string, number, boolean] = ["Bob", 25, true];

使用元組的主要優勢之一是,它們提供了一種在集合中表達特定類型關係的方式。

此外,你可以使用解構賦值來提取元組的元素並將它們分配給變量:

let point: [number, number] = [1, 2];
let [x, y] = point;
console.log(x, y);

最佳實踐 7:使用 any 類型

有時,我們可能沒有有關變量類型的所有信息,但仍然需要在代碼中使用它。在這種情況下,我們可以利用 any 類型。但是,像任何強大的工具一樣,使用 any 應該謹慎和有目的地使用。

使用 any 的一個最佳實踐是將其使用限制在真正未知類型的特定情況下,例如在使用第三方庫或動態生成的數據時。此外,最好添加類型斷言或類型保護,以確保變量被正確使用。儘可能縮小變量類型的範圍。

例如:

function logData(data: any) {
console.log(data);
}

const user = { name: "John", age: 30 };
const numbers = [1, 2, 3];

logData(user); // { name: "John", age: 30 }
logData(numbers); // [1, 2, 3]

另一個最佳實踐是避免在函數返回類型和函數參數中使用 any,因爲它可能會削弱代碼的類型安全性。相反,你可以使用更具體的類型或使用一些提供一定程度類型安全的更通用的類型,如 unknown 或 object。

最佳實踐 8:使用 unknown 類型

unknown 類型是 TypeScript 3.0 中引入的一種強大且限制性更強的類型。它比 any 類型更具限制性,並可以幫助你防止意外的類型錯誤。

any 不同的是,當你使用 unknown 類型時,除非你首先檢查其類型,否則 TypeScript 不允許你對值執行任何操作。這可以幫助你在編譯時捕捉到類型錯誤,而不是在運行時。

例如,你可以使用 unknown 類型創建一個更加類型安全的函數:

function printValue(value: unknown) {
 if (typeof value === "string") {
 console.log(value);
 } else {
 console.log("Not a string");
 }
}

你也可以使用 unknown 類型創建更加類型安全的變量:

let value: unknown = "hello";
let str: string = value; // Error: Type 'unknown' is not assignable to type 'string'.

最佳實踐 9:“never”

在 TypeScript 中,never 是一個特殊的類型,表示永遠不會發生的值。它用於指示函數不會正常返回,而是會拋出錯誤。這是一種很好的方式,可以向其他開發人員(和編譯器)指示一個函數不能以某種方式使用,這可以幫助捕捉潛在的錯誤。

例如,考慮以下函數,如果輸入小於 0,則會拋出錯誤:

function divide(numerator: number, denominator: number): number {
 if (denominator === 0) {
 throw new Error("Cannot divide by zero");
 }
 return numerator / denominator;
}

這裏,函數 divide 聲明爲返回一個數字,但如果分母爲零,則會拋出錯誤。爲了指示在這種情況下該函數不會正常返回,你可以使用 never 作爲返回類型:

function divide(numerator: number, denominator: number): number | never {
 if (denominator === 0) {
 throw new Error("Cannot divide by zero");
 }
 return numerator / denominator;
}

最佳實踐 10:使用 keyof 運算符

keyof 運算符是 TypeScript 的一個強大功能,可以創建一個表示對象鍵的類型。它可以用於明確指示哪些屬性是對象允許的。

例如,你可以使用 keyof 運算符爲對象創建更可讀和可維護的類型:

interface User {
 name: string;
 age: number;
}
type UserKeys = keyof User; // "name" | "age"

你還可以使用 keyof 運算符創建更加類型安全的函數,將對象和鍵作爲參數:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

這將允許你在編譯時檢查 key 是否爲對象 T 的鍵之一,並返回該鍵對應的值。

function getValue<T, K extends keyof T>(obj: T, key: K) {
 return obj[key];
}
let user: User = { name: "John", age: 30 };
console.log(getValue(user, "name")); // "John"
console.log(getValue(user, "gender")); // Error: Argument of type '"gender"' is not assignable to parameter of type '"name" | "age"'.

最佳實踐 11:使用枚舉

枚舉(Enums)是 TypeScript 中定義一組命名常量的一種方式。它們可以用於創建更具可讀性和可維護性的代碼,通過給一組相關的值賦予有意義的名稱。

例如,你可以使用枚舉來定義一個訂單可能的狀態值:

enum OrderStatus {
  Pending,
  Processing,
  Shipped,
  Delivered,
  Cancelled
}
let orderStatus: OrderStatus = OrderStatus.Pending;

枚舉還可以有自定義的一組數字值或字符串值:

enum OrderStatus {
  Pending = 1,
  Processing = 2,
  Shipped = 3,
  Delivered = 4,
  Cancelled = 5
}
let orderStatus: OrderStatus = OrderStatus.Pending;

在命名約定方面,枚舉應該以第一個大寫字母命名,並且名稱應該是單數形式。

最佳實踐 12:使用命名空間

命名空間(Namespaces)是一種組織代碼和防止命名衝突的方法。它們允許你創建一個容器來定義變量、類、函數和接口。

例如,你可以使用命名空間來將所有與特定功能相關的代碼分組:

namespace OrderModule {
  export class Order { /* … / }
  export function cancelOrder(order: Order) { / … / }
  export function processOrder(order: Order) { / … */ }
}
let order = new OrderModule.Order();
OrderModule.cancelOrder(order);

你也可以使用命名空間來爲你的代碼提供一個獨特的名稱,以防止命名衝突:

namespace MyCompany.MyModule {
 export class MyClass { /* … */ }
}
let myClass = new MyCompany.MyModule.MyClass();

需要注意的是,命名空間類似於模塊,但它們用於組織代碼和防止命名衝突,而模塊用於加載和執行代碼。

最佳實踐 13:使用實用類型

實用類型(Utility Types)是 TypeScript 中內置的一種特性,提供了一組預定義類型,可以幫助你編寫更好的類型安全代碼。它們允許你執行常見的類型操作,並以更方便的方式操作類型。

例如,你可以使用 Pick 實用類型從對象類型中提取一組屬性:

type User = { name: string, age: number, email: string };
type UserInfo = Pick<User, "name" | "email">;

你也可以使用 Exclude 實用類型從對象類型中刪除屬性:

type User = { name: string, age: number, email: string };
type UserWithoutAge = Exclude<User, "age">;

你可以使用 Partial 實用類型將類型的所有屬性設置爲可選的:

type User = { name: string, age: number, email: string };
type PartialUser = Partial<User>;

除了上述實用類型外,還有許多其他實用類型,如 Readonly、Record、Omit、Required 等,可以幫助你編寫更好的類型安全代碼。

最佳實踐 14:“只讀” 和 “只讀數組”

當在 TypeScript 中處理數據時,你可能希望確保某些值無法更改。這就是 “只讀” 和“只讀數組”的用武之地。

“只讀” 關鍵字用於使對象的屬性只讀,意味着在創建後它們無法被修改。例如,在處理配置或常量值時,這非常有用。

interface Point {
  x: number;
  y: number;
}
let point: Readonly<Point> = {x: 0, y: 0};
point.x = 1; // TypeScript會報錯,因爲“point.x”是隻讀的

“只讀數組”與 “只讀” 類似,但是用於數組。它使一個數組變成只讀狀態,在創建後不能被修改。

let numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // TypeScript會報錯,因爲“numbers”是隻讀的

最佳實踐 15: 類型保護

在 TypeScript 中,處理複雜類型時,很難跟蹤變量的不同可能性。類型保護是一種強大的工具,可以根據特定條件縮小變量的類型範圍。

以下是如何使用類型保護檢查變量是否爲數字的示例:

function isNumber(x: any): x is number {
  return typeof x === "number";
}
let value = 3;
if (isNumber(value)) {
  value.toFixed(2); // TypeScript 知道 "value" 是一個數字,因爲有了類型保護
}

類型保護還可以與 “in” 運算符、typeof 運算符和 instanceof 運算符一起使用。

最佳實踐 16:使用泛型

泛型是 TypeScript 的一個強大特性,可以讓你編寫可以與任何類型一起使用的代碼,從而使其更具有可重用性。泛型允許你編寫一個單獨的函數、類或接口,可以與多種類型一起使用,而不必爲每種類型編寫單獨的實現。

例如,你可以使用泛型函數來創建任何類型的數組:

function createArray<T>(length: number, value: T): Array<T> {
  let result = [];
  for (let i = 0; i < length; i++) {
  result[i] = value;
  }
  return result;
}
let names = createArray<string>(3, "Bob");
let numbers = createArray<number>(3, 0);

你也可以使用泛型來創建一個可以處理任何類型數據的類:

class GenericNumber<T> {
 zeroValue: T;
 add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

最佳實踐 17:使用 infer 關鍵字

infer 關鍵字是 TypeScript 的一個強大特性,它允許你從一個類型中提取出變量的類型。

例如,你可以使用 infer 關鍵字爲返回特定類型數組的函數創建更精確的類型:

type ArrayType<T> = T extends (infer U)[] ? U : never;
type MyArray = ArrayType<string[]>; // MyArray 類型是 string

你也可以使用 infer 關鍵字爲返回具有特定屬性的對象的函數創建更精確的類型:

type Person = { name: string, age: number };
type PersonName = keyof Person;
type PersonProperty<T> = T extends { [K in keyof T]: infer U } ? U : never;
type Name = PersonProperty<Person>;

在上面的例子中,我們使用了 infer 關鍵字來提取出對象的屬性類型,這個技巧可以用於創建更準確的類型定義。

type ObjectType<T> = T extends { [key: string]: infer U } ? U : never;
type MyObject = ObjectType<{ name: string, age: number }>; // MyObject is of type {name:string, age: number}

最佳實踐 18:使用條件類型

條件類型允許我們表達更復雜的類型關係。基於其他類型的條件創建新類型。

例如,可以使用條件類型來提取函數的返回類型:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<() => void>; // void

還可以使用條件類型來提取對象類型的屬性,滿足特定條件:

type PickProperties<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
type P1 = PickProperties<{ a: number, b: string, c: boolean }, string | number>; // "a" | "b"

最佳實踐 19:使用映射類型

映射類型是一種基於現有類型創建新類型的方式。通過對現有類型的屬性應用一組操作來創建新類型。

例如,可以使用映射類型創建一個表示現有類型只讀版本的新類型:

type Readonly<T> = { readonly [P in keyof T]: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let readonlyObj: Readonly<typeof obj> = { a: 1, b: "hello" };

還可以使用映射類型創建一個表示現有類型可選版本的新類型:

type Optional<T> = { [P in keyof T]?: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let optionalObj: Optional<typeof obj> = { a: 1 };

映射類型可以以不同的方式使用:創建新類型、從現有類型中添加或刪除屬性,或更改現有類型的屬性類型。

最佳實踐 20:使用裝飾器

裝飾器是一種使用簡單語法來爲類、方法或屬性添加額外功能的方式。它們是一種增強類的行爲而不修改其實現的方式。

例如,可以使用裝飾器爲方法添加日誌記錄:

  function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    let originalMethod = descriptor.value;
    descriptor.value = function(…args: any[]) {
    console.log(Calling ${propertyKey} with args: ${JSON.stringify(args)});
    let result = originalMethod.apply(this, args);
    console.log(Called ${propertyKey}, result: ${result});
    return result;
  }
}
class Calculator {
  @logMethod
  add(x: number, y: number): number {
    return x + y;
  }
}

還可以使用裝飾器爲類、方法或屬性添加元數據,這些元數據可以在運行時使用。

function setApiPath(path: string) {
 return function (target: any) {
 target.prototype.apiPath = path;
 }
}
@setApiPath("/users")
class UserService {
 // …
}
console.log(new UserService().apiPath); // "/users"

總結

本文主要介紹了 TypeScript 的 20 個最佳實踐,旨在提高代碼質量和開發效率。其中,一些最佳實踐包括儘可能使用 TypeScript 的類型系統、使用函數和方法參數默認值、使用可選鏈操作符等。此外,該文章還強調了在使用類時,應該使用訪問修飾符,以避免出現不必要的錯誤。

該文章指出,使用 TypeScript 的類型系統可以幫助開發人員避免一些常見的錯誤,例如在運行時引發異常。此外,還提供了一些關於如何編寫類型註釋的最佳實踐。例如,應該儘可能使用函數和方法參數默認值,以避免參數爲空或未定義時的錯誤。

文章中還介紹了一些如何使用 TypeScript 的高級特性的最佳實踐,例如使用類型別名和枚舉,以提高代碼的可讀性和可維護性。此外,該文章還強調了如何使用可選鏈操作符來避免一些運行時錯誤。

總之,該文章提供了許多有用的 TypeScript 最佳實踐,這些實踐可以幫助開發人員編寫更高質量的代碼,提高開發效率,避免一些常見的錯誤。


原文:https://itnext.io/mastering-typescript-21-best-practices-for-improved-code-quality-2f7615e1fdc3

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RjH1yvAL6tDx3ZTWqdGHcA