英文 | https://www.digitalocean.com/community/tutorials/how-to-create-custom-types-in-typescript
翻譯 | 楊小愛
介紹
TypeScript 是 JavaScript 語言的擴充,它使用 JavaScript 運作時和編譯時類型檢查器。
這種組合允許開發人員使用完整的 JavaScript 生态系統和語言功能,同時還添加可選的靜态類型檢查、枚舉資料類型、類和接口。這些特性為開發人員提供了 JavaScript 動态特性的靈活性,但也允許更可靠的代碼庫,其中可以在編譯時使用類型資訊來檢測可能在運作時導緻錯誤或其他意外行為的問題。
額外的類型資訊還提供了更好的代碼庫文檔,并在文本編輯器中改進了 IntelliSense(代碼完成、參數資訊和類似的内容輔助功能)。隊友可以準确地确定任何變量或函數參數的預期類型,而無需通過實作本身。
本教程将介紹類型聲明和 TypeScript 中使用的所有基本類型。它将引導我們完成具有不同代碼示例的示例,我們可以在自己的 TypeScript 環境或 TypeScript Playground(一個允許我們直接在浏覽器中編寫 TypeScript 的線上環境)中跟随這些示例。
準備工作
要遵循本教程,我們将需要:
1)、一個環境,我們可以在其中執行 TypeScript 程式以跟随示例。要在本地計算機上進行設定,我們将需要以下内容。
- 為了運作處理 TypeScript 相關包的開發環境,同時安裝了 Node 和 npm(或 yarn)。本教程使用 Node.js 版本 14.3.0 和 npm 版本 6.14.5 進行了測試。要在 macOS 或 Ubuntu 18.04 上安裝,請按照如何在 macOS 上安裝 Node.js 和建立本地開發環境或如何在 Ubuntu 18.04 上安裝 Node.js 的使用 PPA 安裝部分中的步驟進行操作。如果您使用的是适用于 Linux 的 Windows 子系統 (WSL),這也适用。
- 此外,我們需要在機器上安裝 TypeScript 編譯器 (tsc)。為此,請參閱官方 TypeScript 網站。
2)、如果你不想在本地機器上建立 TypeScript 環境,你可以使用官方的 TypeScript Playground 來跟随。
3)、我們将需要足夠的 JavaScript 知識,尤其是 ES6+ 文法,例如解構、rest 運算符和導入/導出。有關JavaScript的更多主題資訊,建議閱讀我們的 JavaScript 系列教程。
4)、本教程将參考支援 TypeScript 并顯示内聯錯誤的文本編輯器的各個方面。這不是使用 TypeScript 所必需的,但确實可以更多地利用 TypeScript 功能。為了獲得這些好處,我們可以使用像 Visual Studio Code 這樣的文本編輯器,它完全支援開箱即用的 TypeScript。我們也可以在 TypeScript Playground 中嘗試這些好處。
本教程中顯示的所有示例都是使用 TypeScript 4.2.2 版建立的。
建立自定義類型
在程式具有複雜資料結構的情況下,使用 TypeScript 的基本類型可能無法完全描述我們正在使用的資料結構。在這些情況下,聲明自己的類型将幫助我們解決複雜性。在本節中,我們将建立可用于描述我們需要在代碼中使用的任何對象形狀的類型。
自定義類型文法
在 TypeScript 中,建立自定義類型的文法是使用 type 關鍵字,後跟類型名稱,然後使用類型屬性配置設定給 {} 塊。采取以下措施:
type Programmer = {
name: string;
knownFor: string[];
};
文法類似于對象文字,其中鍵是屬性的名稱,值是該屬性應具有的類型。這定義了一個 Programmer 類型,它必須是一個對象,其 name 鍵儲存一個字元串值,并且 knownFor 鍵儲存一個字元串數組。
如前面的示例所示,我們可以使用 ; 作為每個屬性之間的分隔符。也可以使用逗号、, 或完全省略分隔符,如下所示:
type Programmer = {
name: string
knownFor: string[]
};
使用自定義類型與使用任何基本類型相同。添加一個雙冒号,然後添加我們的類型名稱:
type Programmer = {
name: string;
knownFor: string[];
};
const ada: Programmer = {
name: 'Ada Lovelace',
knownFor: ['Mathematics', 'Computing', 'First Programmer']
};
ada 常量現在将通過類型檢查器而不會引發錯誤。
如果我們在任何完全支援 TypeScript 的編輯器中編寫此示例,例如在 TypeScript Playground 中,編輯器将建議該對象期望的字段及其類型,如下面的動畫所示:
如果我們使用 TSDoc 格式(一種流行的 TypeScript 注釋文檔樣式)向字段添加注釋,則在代碼完成中也建議使用它們。使用以下代碼并在注釋中進行解釋:
type Programmer = {
/**
* The full name of the Programmer
*/
name: string;
/**
* This Programmer is known for what?
*/
knownFor: string[];
};
const ada: Programmer = {
name: 'Ada Lovelace',
knownFor: ['Mathematics', 'Computing', 'First Programmer']
};
注釋描述現在将與字段建議一起出現:
在使用自定義類型 Programmer 建立對象時,如果我們為任何屬性配置設定具有意外類型的值,TypeScript 将抛出錯誤。采用以下代碼塊,其中突出顯示的行不符合類型聲明:
type Programmer = {
name: string;
knownFor: string[];
};
const ada: Programmer = {
name: true,
knownFor: ['Mathematics', 'Computing', 'First Programmer']
};
TypeScript 編譯器 (tsc) 将顯示錯誤 2322:
Type 'boolean' is not assignable to type 'string'. (2322)
如果我們省略了我們的類型所需的任何屬性,如下所示:
type Programmer = {
name: string;
knownFor: string[];
};
const ada: Programmer = {
name: 'Ada Lovelace'
};
TypeScript 編譯器将給出錯誤 2741:
Property 'knownFor' is missing in type '{ name: string; }' but required in type 'Programmer'. (2741)
添加原始類型中未指定的新屬性也會導緻錯誤:
type Programmer = {
name: string;
knownFor: string[];
};
const ada: Programmer = {
name: "Ada Lovelace",
knownFor: ['Mathematics', 'Computing', 'First Programmer'],
age: 36
};
在這種情況下,顯示的錯誤是 2322:
Type '{ name: string; knownFor: string[]; age: number; }' is not assignable to type 'Programmer'.
Object literal may only specify known properties, and 'age' does not exist in type 'Programmer'.(2322)
嵌套自定義類型
我們還可以将自定義類型嵌套在一起。想象一下,我們有一個 Company 類型,它有一個符合 Person 類型的 manager 字段。我們可以像這樣建立這些類型:
type Person = {
name: string;
};
type Company = {
name: string;
manager: Person;
};
然後,我們可以像這樣建立一個 Company 類型的值:
const manager: Person = {
name: 'John Doe',
}
const company: Company = {
name: 'ACME',
manager,
}
此代碼将通過類型檢查器,因為管理器常量符合為管理器字段指定的類型。請注意,這使用對象屬性簡寫來聲明管理器。
我們可以省略 manager 常量中的類型,因為它與 Person 類型具有相同的形狀。當我們使用與 manager 屬性類型所期望的形狀相同的對象時,TypeScript 不會引發錯誤,即使它沒有明确設定為 Person 類型。
以下不會引發錯誤:
const manager = {
name: 'John Doe'
}
const company: Company = {
name: 'ACME',
manager
}
我們甚至可以更進一步,直接在company對象字面量中設定manager:
const company: Company = {
name: 'ACME',
manager: {
name: 'John Doe'
}
};
所有這些場景都是有效的。
如果在支援 TypeScript 的編輯器中編寫這些示例,我們會發現編輯器将使用可用的類型資訊來記錄自己。對于前面的示例,隻要我們打開 manager 的 {} 對象文字,編輯器就會期望一個name類型的字元串屬性:
現在,我們已經完成了一些使用固定數量的屬性建立我們自己的自定義類型的示例,接下來,我們将嘗試向我們的類型添加可選屬性。
可選屬性
使用前面部分中的自定義類型聲明,我們在建立具有該類型的值時不能省略任何屬性。但是,有些情況需要可選屬性,這些屬性可以通過類型檢查器(帶或不帶值)。在本節中,我們将聲明這些可選屬性。
要将可選屬性添加到類型,請添加 ? 屬性的修飾符。使用前面部分中的 Programmer 類型,通過添加以下突出顯示的字元将 knownFor 屬性轉換為可選屬性:
type Programmer = {
name: string;
knownFor?: string[];
};
在這裡我們要添加 ? 屬性名稱後的修飾符。這使得 TypeScript 将此屬性視為可選的,并且在我們省略該屬性時不會引發錯誤:
type Programmer = {
name: string;
knownFor?: string[];
};
const ada: Programmer = {
name: 'Ada Lovelace'
};
這将毫無錯誤地通過。
既然,我們已經知道如何向類型添加可選屬性,那麼,現在該學習如何建立一個可以容納無限數量的字段的類型了。
可索引類型
前面的示例表明,如果該類型在聲明時未指定這些屬性,則無法将屬性添加到給定類型的值。在本節中,我們将建立可索引類型,這些類型允許任意數量的字段(如果它們遵循該類型的索引簽名)。
想象一下,我們有一個 Data 類型來儲存任何類型的無限數量的屬性。我們可以像這樣聲明這個類型:
type Data = {
[key: string]: any;
};
在這裡,我們使用大括号 ({}) 中的類型定義塊建立一個普通類型,然後以 [key: typeOfKeys]: typeOfValues 的格式添加一個特殊屬性,其中 typeOfKeys 是該對象的鍵應具有的類型, typeOfValues 是這些鍵的值應該具有的類型。
然後,我們可以像任何其他類型一樣正常使用它:
type Data = {
[key: string]: any;
};
const someData: Data = {
someBooleanKey: true,
someStringKey: 'text goes here'
// ...
}
使用可索引類型,我們可以配置設定無限數量的屬性,隻要它們與索引簽名比對,索引簽名是用于描述可索引類型的鍵和值的類型的名稱。在這種情況下,鍵具有字元串類型,值具有任何類型。
還可以将始終需要的特定屬性添加到可索引類型中,就像使用普通類型一樣。在以下突出顯示的代碼中,我們将狀态屬性添加到我們的資料類型:
type Data = {
status: boolean;
[key: string]: any;
};
const someData: Data = {
status: true,
someBooleanKey: true,
someStringKey: 'text goes here'
// ...
}
這意味着資料類型對象必須有一個帶有布爾值的狀态鍵才能通過類型檢查器。
現在,我們可以建立具有不同數量元素的對象,我們可以繼續學習 TypeScript 中的數組,它可以具有自定義數量的元素或更多。
建立元素數量或更多的數組
使用 TypeScript 中可用的數組和元組基本類型,我們可以為應該具有最少元素的數組建立自定義類型。在本節中,我們将使用 TypeScript 剩餘運算符...來執行此操作。
想象一下,我們有一個負責合并多個字元串的函數。此函數将采用單個數組參數。這個數組必須至少有兩個元素,每個元素都應該是字元串。我們可以使用以下内容建立這樣的類型:
type MergeStringsArray = [string, string, ...string[]];
MergeStringsArray 類型利用了這樣一個事實,即我們可以将 rest 運算符與數組類型一起使用,并将其結果用作元組的第三個元素。這意味着前兩個字元串是必需的,但之後的其他字元串元素不是必需的。
如果一個數組的字元串元素少于兩個,它将是無效的,如下所示:
const invalidArray: MergeStringsArray = ['some-string']
TypeScript 編譯器在檢查此數組時将給出錯誤 2322:
Type '[string]' is not assignable to type 'MergeStringsArray'.
Source has 1 element(s) but target requires 2. (2322)
到目前為止,我們已經從基本類型的組合中建立了自己的自定義類型。在下一節中,我們将通過将兩個或多個自定義類型組合在一起來建立一個新類型。
組合類型
在這裡我們将介紹兩種組合類型的方法。這些将使用聯合運算符傳遞符合一種或另一種類型的任何資料,并使用交集運算符傳遞滿足兩種類型中所有條件的資料。
Unions
unions是使用 | 建立的 (pipe) 運算符,它表示可以具有聯合中任何類型的值。舉個例子:
type ProductCode = number | string
在此代碼中,ProductCode 可以是字元串或數字。以下代碼将通過類型檢查器:
type ProductCode = number | string;
const productCodeA: ProductCode = 'this-works';
const productCodeB: ProductCode = 1024;
unions類型可以從任何有效 TypeScript 類型的聯合中建立。
Intersections
我們可以使用相交類型來建立一個全新的類型,該類型具有相交在一起的所有類型的所有屬性。
例如,假設我們有一些公共字段始終出現在 API 調用的響應中,然後是某些端點的特定字段:
type StatusResponse = {
status: number;
isValid: boolean;
};
type User = {
name: string;
};
type GetUserResponse = {
user: User;
};
在這種情況下,所有響應都将具有 status 和 isValid 屬性,但隻有使用者響應将具有附加的使用者字段。要使用交集類型建立特定 API 使用者調用的結果響應,請結合使用 StatusResponse 和 GetUserResponse 類型:
type ApiGetUserResponse = StatusResponse & GetUserResponse;
ApiGetUserResponse 類型将具有 StatusResponse 中可用的所有屬性以及 GetUserResponse 中可用的屬性。這意味着資料隻有在滿足兩種類型的所有條件時才會通過類型檢查器。以下示例将起作用:
let response: ApiGetUserResponse = {
status: 200,
isValid: true,
user: {
name: 'Sammy'
}
}
另一個示例是資料庫用戶端為包含連接配接的查詢傳回的行類型。我們将能夠使用交集類型來指定此類查詢的結果:
type UserRoleRow = {
role: string;
}
type UserRow = {
name: string;
};
type UserWithRoleRow = UserRow & UserRoleRow;
稍後,如果我們使用 fetchRowsFromDatabase() 函數,如下所示:
const joinedRows: UserWithRoleRow = fetchRowsFromDatabase()
生成的常量joinedRows 必須有一個role 屬性和一個name 屬性,它們都儲存字元串值,以便通過類型檢查器。
使用模闆字元串類型
從 TypeScript 4.1 開始,可以使用模闆字元串類型建立類型。這将允許我們建立檢查特定字元串格式的類型,并為我們的 TypeScript 項目添加更多自定義。
要建立模闆字元串類型,我們使用的文法與建立模闆字元串文字時使用的文法幾乎相同。但是,我們将在字元串模闆中使用其他類型而不是值。
想象一下,我們想建立一個傳遞所有以 get 開頭的字元串的類型。我們可以使用模闆字元串類型來做到這一點:
type StringThatStartsWithGet = `get${string}`;
const myString: StringThatStartsWithGet = 'getAbc';
myString 将在此處通過類型檢查器,因為字元串以 get 開頭,然後是一個附加字元串。
如果我們将無效值傳遞給我們的類型,例如以下 invalidStringValue:
type StringThatStartsWithGet = `get${string}`;
const invalidStringValue: StringThatStartsWithGet = 'something';
TypeScript 編譯器會給我們錯誤 2322:
Type '"something"' is not assignable to type '`get${string}`'. (2322)
使用模闆字元串建立類型可幫助我們根據項目的特定需求自定義類型。在下一節中,我們将嘗試類型斷言,它為其他無類型資料添加類型。
Using Type Assertions
any 類型可以用作 any 值的類型,這通常不提供充分利用 TypeScript 所需的強類型。但有時我們可能最終會得到一些與我們無法控制的變量綁定的變量。如果我們使用的外部依賴項不是用 TypeScript 編寫的,或者沒有可用的類型聲明,就會發生這種情況。
如果我們想讓我們的代碼在這些場景中是類型安全的,我們可以使用類型斷言,這是一種将變量類型更改為另一種類型的方法。通過在變量後添加 as NewType 可以實作類型斷言。這會将變量的類型更改為 as 關鍵字之後指定的類型。
舉個例子:
const valueA: any = 'something';
const valueB = valueA as string;
value 的類型為 any,但是,使用 as 關鍵字,此代碼将 value 強制為 string 類型。
注意:要斷言 TypeA 的變量具有 TypeB 類型,TypeB 必須是 TypeA 的子類型。幾乎所有的 TypeScript 類型,除了 never,都是 any 的子類型,包括 unknown。
實用程式類型
在前面的部分中,我們檢視了從基本類型建立自定義類型的多種方法。但有時我們不想從頭開始建立一個全新的類型。有時最好使用現有類型的一些屬性,甚至建立一個與另一種類型具有相同形狀但所有屬性都設定為可選的新類型。
使用 TypeScript 提供的現有實用程式類型,所有這些都是可能的。本節将介紹其中一些實用程式類型;有關所有可用的完整清單,請檢視 TypeScript 手冊的實用程式類型部分。
所有實用程式類型都是通用類型,我們可以将其視為接受其他類型作為參數的類型。可以通過使用 <TypeA, TypeB, ...> 文法向其傳遞類型參數來識别通用類型。
Record<Key, Value>
Record 實用程式類型可用于以比使用之前介紹的索引簽名更簡潔的方式建立可索引類型。
在我們的可索引類型示例中,我們具有以下類型:
type Data = {
[key: string]: any;
};
我們可以使用 Record 實用程式類型而不是像這樣的可索引類型:
type Data = Record<string, any>;
Record 泛型的第一個類型參數是每個鍵的類型。在以下示例中,所有鍵都必須是字元串:
type Data = Record<string, any>
第二個類型參數是這些鍵的每個值的類型。以下将允許值是任何值:
type Data = Record<string, any>
Omit<Type, Fields>
Omit 實用程式類型可用于基于另一種類型建立新類型,同時排除結果類型中不需要的一些屬性。
假設我們有以下類型來表示資料庫中使用者行的類型:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
如果在我們的代碼中,我們要檢索除 addressId 之外的所有字段,則可以使用 Omit 建立沒有該字段的新類型:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowWithoutAddressId = Omit<UserRow, 'addressId'>;
Omit 的第一個參數是新類型所基于的類型。第二個是我們要省略的字段。
如果我們在代碼編輯器中将滑鼠懸停在 UserRowWithoutAddressId 上,我們會發現它具有 UserRow 類型的所有屬性,但我們省略了這些屬性。
我們可以使用字元串聯合将多個字段傳遞給第二個類型參數。假設我們還想省略 id 字段,我們可以這樣做:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowWithoutIds = Omit<UserRow, 'id' | 'addressId'>;
Pick<Type, Fields>
Pick 實用程式類型與 Omit 類型完全相反。我們無需說出要省略的字段,而是指定要從其他類型使用的字段。
使用我們之前使用的相同 UserRow:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
假設我們隻需要從資料庫行中選擇電子郵件鍵。我們可以像這樣使用 Pick 建立這樣的類型:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowWithEmailOnly = Pick<UserRow, 'email'>;
Pick 這裡的第一個參數指定了新類型所基于的類型。第二個是我們想要包含的鍵。
這将等同于以下内容:
type UserRowWithEmailOnly = {
email: string;
}
我們還可以使用字元串聯合來選擇多個字段:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowWithEmailOnly = Pick<UserRow, 'name' | 'email'>;
Partial<Type>
使用相同的 UserRow 示例,假設我們想建立一個新類型,該類型與我們的資料庫用戶端可以用來将新資料插入使用者表中的對象相比對,但有一個小細節:我們的資料庫具有所有字段的預設值,是以,我們是不需要通過其中任何一個。
為此,我們可以使用 Partial 實用程式類型來選擇性地包括基本類型的所有字段。
我們現有的類型 UserRow 具有所需的所有屬性:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
要建立所有屬性都是可選的新類型,我們可以使用 Partial<Type> 實用程式類型,如下所示:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowInsert = Partial<UserRow>;
這與擁有這樣的 UserRowInsert 完全相同:
type UserRow = {
id: number;
name: string;
email: string;
addressId: string;
};
type UserRowInsert = {
id?: number | undefined;
name?: string | undefined;
email?: string | undefined;
addressId?: string | undefined;
};
實用程式類型是一個很好的資源,因為它們提供了一種比從 TypeScript 中的基本類型建立類型更快的方法來建構類型。
總結
建立我們自己的自定義類型來表示我們自己的代碼中使用的資料結構,可以為我們的項目提供靈活且有用的 TypeScript 解決方案。除了從整體上提高我們自己代碼的類型安全性之外,将我們自己的業務對象類型化為代碼中的資料結構将增加代碼庫的整體文檔,并在與團隊成員一起工作時改善我們自己的開發人員體驗相同的代碼庫。
學習更多技能
請點下方公衆号