如何在 TypeScript 中創(chuàng)建自定義類型

英文 | https://www.digitalocean.com/community/tutorials/how-to-create-custom-types-in-typescript
翻譯 | 楊小愛
為了運(yùn)行處理 TypeScript 相關(guān)包的開發(fā)環(huán)境,同時(shí)安裝了 Node 和 npm(或 yarn)。本教程使用 Node.js 版本 14.3.0 和 npm 版本 6.14.5 進(jìn)行了測試。要在 macOS 或 Ubuntu 18.04 上安裝,請按照如何在 macOS 上安裝 Node.js 和創(chuàng)建本地開發(fā)環(huán)境或如何在 Ubuntu 18.04 上安裝 Node.js 的使用 PPA 安裝部分中的步驟進(jìn)行操作。如果您使用的是適用于 Linux 的 Windows 子系統(tǒng) (WSL),這也適用。
此外,我們需要在機(jī)器上安裝 TypeScript 編譯器 (tsc)。為此,請參閱官方 TypeScript 網(wǎng)站。
創(chuàng)建自定義類型
在程序具有復(fù)雜數(shù)據(jù)結(jié)構(gòu)的情況下,使用 TypeScript 的基本類型可能無法完全描述我們正在使用的數(shù)據(jù)結(jié)構(gòu)。在這些情況下,聲明自己的類型將幫助我們解決復(fù)雜性。在本節(jié)中,我們將創(chuàng)建可用于描述我們需要在代碼中使用的任何對象形狀的類型。
自定義類型語法
在 TypeScript 中,創(chuàng)建自定義類型的語法是使用 type 關(guān)鍵字,后跟類型名稱,然后使用類型屬性分配給 {} 塊。采取以下措施:
type Programmer = {name: string;knownFor: string[];};
語法類似于對象文字,其中鍵是屬性的名稱,值是該屬性應(yīng)具有的類型。這定義了一個(gè) Programmer 類型,它必須是一個(gè)對象,其 name 鍵保存一個(gè)字符串值,并且 knownFor 鍵保存一個(gè)字符串?dāng)?shù)組。
如前面的示例所示,我們可以使用 ; 作為每個(gè)屬性之間的分隔符。也可以使用逗號、, 或完全省略分隔符,如下所示:
type Programmer = {name: stringknownFor: string[]};
type Programmer = {name: string;knownFor: string[];};const ada: Programmer = {name: 'Ada Lovelace',knownFor: ['Mathematics', 'Computing', 'First Programmer']};
ada 常量現(xiàn)在將通過類型檢查器而不會引發(fā)錯(cuò)誤。
如果我們在任何完全支持 TypeScript 的編輯器中編寫此示例,例如在 TypeScript Playground 中,編輯器將建議該對象期望的字段及其類型,如下面的動(dòng)畫所示:

如果我們使用 TSDoc 格式(一種流行的 TypeScript 注釋文檔樣式)向字段添加注釋,則在代碼完成中也建議使用它們。使用以下代碼并在注釋中進(jìn)行解釋:
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']};
注釋描述現(xiàn)在將與字段建議一起出現(xiàn):

在使用自定義類型 Programmer 創(chuàng)建對象時(shí),如果我們?yōu)槿魏螌傩苑峙渚哂幸馔忸愋偷闹?,TypeScript 將拋出錯(cuò)誤。采用以下代碼塊,其中突出顯示的行不符合類型聲明:
type Programmer = {name: string;knownFor: string[];};const ada: Programmer = {name: true,knownFor: ['Mathematics', 'Computing', 'First Programmer']};
TypeScript 編譯器 (tsc) 將顯示錯(cuò)誤 2322:
Type 'boolean' is not assignable to type 'string'. (2322)如果我們省略了我們的類型所需的任何屬性,如下所示:
type Programmer = {name: string;knownFor: string[];};const ada: Programmer = {name: 'Ada Lovelace'};
TypeScript 編譯器將給出錯(cuò)誤 2741:
Property 'knownFor' is missing in type '{ name: string; }' but required in type 'Programmer'. (2741)添加原始類型中未指定的新屬性也會導(dǎo)致錯(cuò)誤:
type Programmer = {name: string;knownFor: string[];};const ada: Programmer = {name: "Ada Lovelace",knownFor: ['Mathematics', 'Computing', 'First Programmer'],age: 36};
在這種情況下,顯示的錯(cuò)誤是 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)
嵌套自定義類型
我們還可以將自定義類型嵌套在一起。想象一下,我們有一個(gè) Company 類型,它有一個(gè)符合 Person 類型的 manager 字段。我們可以像這樣創(chuàng)建這些類型:
type Person = {name: string;};type Company = {name: string;manager: Person;};
然后,我們可以像這樣創(chuàng)建一個(gè) Company 類型的值:
const manager: Person = {name: 'John Doe',}const company: Company = {name: 'ACME',manager,}
此代碼將通過類型檢查器,因?yàn)楣芾砥鞒A糠蠟楣芾砥髯侄沃付ǖ念愋汀U堊⒁?,這使用對象屬性簡寫來聲明管理器。
我們可以省略 manager 常量中的類型,因?yàn)樗c Person 類型具有相同的形狀。當(dāng)我們使用與 manager 屬性類型所期望的形狀相同的對象時(shí),TypeScript 不會引發(fā)錯(cuò)誤,即使它沒有明確設(shè)置為 Person 類型。
以下不會引發(fā)錯(cuò)誤:
const manager = {name: 'John Doe'}const company: Company = {name: 'ACME',manager}
我們甚至可以更進(jìn)一步,直接在company對象字面量中設(shè)置manager:
const company: Company = {name: 'ACME',manager: {name: 'John Doe'}};
所有這些場景都是有效的。
如果在支持 TypeScript 的編輯器中編寫這些示例,我們會發(fā)現(xiàn)編輯器將使用可用的類型信息來記錄自己。對于前面的示例,只要我們打開 manager 的 {} 對象文字,編輯器就會期望一個(gè)name類型的字符串屬性:

現(xiàn)在,我們已經(jīng)完成了一些使用固定數(shù)量的屬性創(chuàng)建我們自己的自定義類型的示例,接下來,我們將嘗試向我們的類型添加可選屬性。
可選屬性
使用前面部分中的自定義類型聲明,我們在創(chuàng)建具有該類型的值時(shí)不能省略任何屬性。但是,有些情況需要可選屬性,這些屬性可以通過類型檢查器(帶或不帶值)。在本節(jié)中,我們將聲明這些可選屬性。
要將可選屬性添加到類型,請?zhí)砑?? 屬性的修飾符。使用前面部分中的 Programmer 類型,通過添加以下突出顯示的字符將 knownFor 屬性轉(zhuǎn)換為可選屬性:
type Programmer = {name: string;knownFor?: string[];};
在這里我們要添加 ? 屬性名稱后的修飾符。這使得 TypeScript 將此屬性視為可選的,并且在我們省略該屬性時(shí)不會引發(fā)錯(cuò)誤:
type Programmer = {name: string;knownFor?: string[];};const ada: Programmer = {name: 'Ada Lovelace'};
這將毫無錯(cuò)誤地通過。
既然,我們已經(jīng)知道如何向類型添加可選屬性,那么,現(xiàn)在該學(xué)習(xí)如何創(chuàng)建一個(gè)可以容納無限數(shù)量的字段的類型了。
可索引類型
前面的示例表明,如果該類型在聲明時(shí)未指定這些屬性,則無法將屬性添加到給定類型的值。在本節(jié)中,我們將創(chuàng)建可索引類型,這些類型允許任意數(shù)量的字段(如果它們遵循該類型的索引簽名)。
想象一下,我們有一個(gè) Data 類型來保存任何類型的無限數(shù)量的屬性。我們可以像這樣聲明這個(gè)類型:
type Data = {[key: string]: any;};
在這里,我們使用大括號 ({}) 中的類型定義塊創(chuàng)建一個(gè)普通類型,然后以 [key: typeOfKeys]: typeOfValues 的格式添加一個(gè)特殊屬性,其中 typeOfKeys 是該對象的鍵應(yīng)具有的類型, typeOfValues 是這些鍵的值應(yīng)該具有的類型。
然后,我們可以像任何其他類型一樣正常使用它:
type Data = {[key: string]: any;};const someData: Data = {someBooleanKey: true,someStringKey: 'text goes here'// ...}
使用可索引類型,我們可以分配無限數(shù)量的屬性,只要它們與索引簽名匹配,索引簽名是用于描述可索引類型的鍵和值的類型的名稱。在這種情況下,鍵具有字符串類型,值具有任何類型。
還可以將始終需要的特定屬性添加到可索引類型中,就像使用普通類型一樣。在以下突出顯示的代碼中,我們將狀態(tài)屬性添加到我們的數(shù)據(jù)類型:
type Data = {status: boolean;[key: string]: any;};const someData: Data = {status: true,someBooleanKey: true,someStringKey: 'text goes here'// ...}
這意味著數(shù)據(jù)類型對象必須有一個(gè)帶有布爾值的狀態(tài)鍵才能通過類型檢查器。
現(xiàn)在,我們可以創(chuàng)建具有不同數(shù)量元素的對象,我們可以繼續(xù)學(xué)習(xí) TypeScript 中的數(shù)組,它可以具有自定義數(shù)量的元素或更多。
創(chuàng)建元素?cái)?shù)量或更多的數(shù)組
使用 TypeScript 中可用的數(shù)組和元組基本類型,我們可以為應(yīng)該具有最少元素的數(shù)組創(chuàng)建自定義類型。在本節(jié)中,我們將使用 TypeScript 剩余運(yùn)算符...來執(zhí)行此操作。
想象一下,我們有一個(gè)負(fù)責(zé)合并多個(gè)字符串的函數(shù)。此函數(shù)將采用單個(gè)數(shù)組參數(shù)。這個(gè)數(shù)組必須至少有兩個(gè)元素,每個(gè)元素都應(yīng)該是字符串。我們可以使用以下內(nèi)容創(chuàng)建這樣的類型:
type MergeStringsArray = [string, string, ...string[]];MergeStringsArray 類型利用了這樣一個(gè)事實(shí),即我們可以將 rest 運(yùn)算符與數(shù)組類型一起使用,并將其結(jié)果用作元組的第三個(gè)元素。這意味著前兩個(gè)字符串是必需的,但之后的其他字符串元素不是必需的。
如果一個(gè)數(shù)組的字符串元素少于兩個(gè),它將是無效的,如下所示:
const invalidArray: MergeStringsArray = ['some-string']TypeScript 編譯器在檢查此數(shù)組時(shí)將給出錯(cuò)誤 2322:
Type '[string]' is not assignable to type 'MergeStringsArray'.Source has 1 element(s) but target requires 2. (2322)
到目前為止,我們已經(jīng)從基本類型的組合中創(chuàng)建了自己的自定義類型。在下一節(jié)中,我們將通過將兩個(gè)或多個(gè)自定義類型組合在一起來創(chuàng)建一個(gè)新類型。
組合類型
在這里我們將介紹兩種組合類型的方法。這些將使用聯(lián)合運(yùn)算符傳遞符合一種或另一種類型的任何數(shù)據(jù),并使用交集運(yùn)算符傳遞滿足兩種類型中所有條件的數(shù)據(jù)。
Unions
unions是使用 | 創(chuàng)建的 (pipe) 運(yùn)算符,它表示可以具有聯(lián)合中任何類型的值。舉個(gè)例子:
type ProductCode = number | string在此代碼中,ProductCode 可以是字符串或數(shù)字。以下代碼將通過類型檢查器:
type ProductCode = number | string;const productCodeA: ProductCode = 'this-works';const productCodeB: ProductCode = 1024;
unions類型可以從任何有效 TypeScript 類型的聯(lián)合中創(chuàng)建。
Intersections
我們可以使用相交類型來創(chuàng)建一個(gè)全新的類型,該類型具有相交在一起的所有類型的所有屬性。
例如,假設(shè)我們有一些公共字段始終出現(xiàn)在 API 調(diào)用的響應(yīng)中,然后是某些端點(diǎn)的特定字段:
type StatusResponse = {status: number;isValid: boolean;};type User = {name: string;};type GetUserResponse = {user: User;};
在這種情況下,所有響應(yīng)都將具有 status 和 isValid 屬性,但只有用戶響應(yīng)將具有附加的用戶字段。要使用交集類型創(chuàng)建特定 API 用戶調(diào)用的結(jié)果響應(yīng),請結(jié)合使用 StatusResponse 和 GetUserResponse 類型:
type ApiGetUserResponse = StatusResponse & GetUserResponse;ApiGetUserResponse 類型將具有 StatusResponse 中可用的所有屬性以及 GetUserResponse 中可用的屬性。這意味著數(shù)據(jù)只有在滿足兩種類型的所有條件時(shí)才會通過類型檢查器。以下示例將起作用:
let response: ApiGetUserResponse = {status: 200,isValid: true,user: {name: 'Sammy'}}
另一個(gè)示例是數(shù)據(jù)庫客戶端為包含連接的查詢返回的行類型。我們將能夠使用交集類型來指定此類查詢的結(jié)果:
type UserRoleRow = {role: string;}type UserRow = {name: string;};type UserWithRoleRow = UserRow & UserRoleRow;
稍后,如果我們使用 fetchRowsFromDatabase() 函數(shù),如下所示:
const joinedRows: UserWithRoleRow = fetchRowsFromDatabase()生成的常量joinedRows 必須有一個(gè)role 屬性和一個(gè)name 屬性,它們都保存字符串值,以便通過類型檢查器。
使用模板字符串類型
從 TypeScript 4.1 開始,可以使用模板字符串類型創(chuàng)建類型。這將允許我們創(chuàng)建檢查特定字符串格式的類型,并為我們的 TypeScript 項(xiàng)目添加更多自定義。
要?jiǎng)?chuàng)建模板字符串類型,我們使用的語法與創(chuàng)建模板字符串文字時(shí)使用的語法幾乎相同。但是,我們將在字符串模板中使用其他類型而不是值。
想象一下,我們想創(chuàng)建一個(gè)傳遞所有以 get 開頭的字符串的類型。我們可以使用模板字符串類型來做到這一點(diǎn):
type StringThatStartsWithGet = `get${string}`;const myString: StringThatStartsWithGet = 'getAbc';
myString 將在此處通過類型檢查器,因?yàn)樽址?get 開頭,然后是一個(gè)附加字符串。
如果我們將無效值傳遞給我們的類型,例如以下 invalidStringValue:
type StringThatStartsWithGet = `get${string}`;const invalidStringValue: StringThatStartsWithGet = 'something';
TypeScript 編譯器會給我們錯(cuò)誤 2322:
Type '"something"' is not assignable to type '`get${string}`'. (2322)使用模板字符串創(chuàng)建類型可幫助我們根據(jù)項(xiàng)目的特定需求自定義類型。在下一節(jié)中,我們將嘗試類型斷言,它為其他無類型數(shù)據(jù)添加類型。
Using Type Assertions
any 類型可以用作 any 值的類型,這通常不提供充分利用 TypeScript 所需的強(qiáng)類型。但有時(shí)我們可能最終會得到一些與我們無法控制的變量綁定的變量。如果我們使用的外部依賴項(xiàng)不是用 TypeScript 編寫的,或者沒有可用的類型聲明,就會發(fā)生這種情況。
如果我們想讓我們的代碼在這些場景中是類型安全的,我們可以使用類型斷言,這是一種將變量類型更改為另一種類型的方法。通過在變量后添加 as NewType 可以實(shí)現(xiàn)類型斷言。這會將變量的類型更改為 as 關(guān)鍵字之后指定的類型。
舉個(gè)例子:
const valueA: any = 'something';const valueB = valueA as string;
value 的類型為 any,但是,使用 as 關(guān)鍵字,此代碼將 value 強(qiáng)制為 string 類型。
注意:要斷言 TypeA 的變量具有 TypeB 類型,TypeB 必須是 TypeA 的子類型。幾乎所有的 TypeScript 類型,除了 never,都是 any 的子類型,包括 unknown。
實(shí)用程序類型
在前面的部分中,我們查看了從基本類型創(chuàng)建自定義類型的多種方法。但有時(shí)我們不想從頭開始創(chuàng)建一個(gè)全新的類型。有時(shí)最好使用現(xiàn)有類型的一些屬性,甚至創(chuàng)建一個(gè)與另一種類型具有相同形狀但所有屬性都設(shè)置為可選的新類型。
使用 TypeScript 提供的現(xiàn)有實(shí)用程序類型,所有這些都是可能的。本節(jié)將介紹其中一些實(shí)用程序類型;有關(guān)所有可用的完整列表,請查看 TypeScript 手冊的實(shí)用程序類型部分。
所有實(shí)用程序類型都是通用類型,我們可以將其視為接受其他類型作為參數(shù)的類型??梢酝ㄟ^使用 <TypeA, TypeB, ...> 語法向其傳遞類型參數(shù)來識別通用類型。
Record<Key, Value>
Record 實(shí)用程序類型可用于以比使用之前介紹的索引簽名更簡潔的方式創(chuàng)建可索引類型。
在我們的可索引類型示例中,我們具有以下類型:
type Data = {[key: string]: any;};
我們可以使用 Record 實(shí)用程序類型而不是像這樣的可索引類型:
type Data = Record<string, any>;Record 泛型的第一個(gè)類型參數(shù)是每個(gè)鍵的類型。在以下示例中,所有鍵都必須是字符串:
type Data = Record<string, any>第二個(gè)類型參數(shù)是這些鍵的每個(gè)值的類型。以下將允許值是任何值:
type Data = Record<string, any>Omit<Type, Fields>
Omit 實(shí)用程序類型可用于基于另一種類型創(chuàng)建新類型,同時(shí)排除結(jié)果類型中不需要的一些屬性。
假設(shè)我們有以下類型來表示數(shù)據(jù)庫中用戶行的類型:
type UserRow = {id: number;name: string;email: string;addressId: string;};
如果在我們的代碼中,我們要檢索除 addressId 之外的所有字段,則可以使用 Omit 創(chuàng)建沒有該字段的新類型:
type UserRow = {id: number;name: string;email: string;addressId: string;};type UserRowWithoutAddressId = Omit<UserRow, 'addressId'>;
Omit 的第一個(gè)參數(shù)是新類型所基于的類型。第二個(gè)是我們要省略的字段。
如果我們在代碼編輯器中將鼠標(biāo)懸停在 UserRowWithoutAddressId 上,我們會發(fā)現(xiàn)它具有 UserRow 類型的所有屬性,但我們省略了這些屬性。
我們可以使用字符串聯(lián)合將多個(gè)字段傳遞給第二個(gè)類型參數(shù)。假設(shè)我們還想省略 id 字段,我們可以這樣做:
type UserRow = {id: number;name: string;email: string;addressId: string;};type UserRowWithoutIds = Omit<UserRow, 'id' | 'addressId'>;
Pick<Type, Fields>
Pick 實(shí)用程序類型與 Omit 類型完全相反。我們無需說出要省略的字段,而是指定要從其他類型使用的字段。
使用我們之前使用的相同 UserRow:
type UserRow = {id: number;name: string;email: string;addressId: string;};
假設(shè)我們只需要從數(shù)據(jù)庫行中選擇電子郵件鍵。我們可以像這樣使用 Pick 創(chuàng)建這樣的類型:
type UserRow = {id: number;name: string;email: string;addressId: string;};type UserRowWithEmailOnly = Pick<UserRow, 'email'>;
Pick 這里的第一個(gè)參數(shù)指定了新類型所基于的類型。第二個(gè)是我們想要包含的鍵。
這將等同于以下內(nèi)容:
type UserRowWithEmailOnly = {email: string;}
我們還可以使用字符串聯(lián)合來選擇多個(gè)字段:
type UserRow = {id: number;name: string;email: string;addressId: string;};type UserRowWithEmailOnly = Pick<UserRow, 'name' | 'email'>;
Partial<Type>
使用相同的 UserRow 示例,假設(shè)我們想創(chuàng)建一個(gè)新類型,該類型與我們的數(shù)據(jù)庫客戶端可以用來將新數(shù)據(jù)插入用戶表中的對象相匹配,但有一個(gè)小細(xì)節(jié):我們的數(shù)據(jù)庫具有所有字段的默認(rèn)值,所以,我們是不需要通過其中任何一個(gè)。
為此,我們可以使用 Partial 實(shí)用程序類型來選擇性地包括基本類型的所有字段。
我們現(xiàn)有的類型 UserRow 具有所需的所有屬性:
type UserRow = {id: number;name: string;email: string;addressId: string;};
要?jiǎng)?chuàng)建所有屬性都是可選的新類型,我們可以使用 Partial<Type> 實(shí)用程序類型,如下所示:
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;};
實(shí)用程序類型是一個(gè)很好的資源,因?yàn)樗鼈兲峁┝艘环N比從 TypeScript 中的基本類型創(chuàng)建類型更快的方法來構(gòu)建類型。
總結(jié)
創(chuàng)建我們自己的自定義類型來表示我們自己的代碼中使用的數(shù)據(jù)結(jié)構(gòu),可以為我們的項(xiàng)目提供靈活且有用的 TypeScript 解決方案。除了從整體上提高我們自己代碼的類型安全性之外,將我們自己的業(yè)務(wù)對象類型化為代碼中的數(shù)據(jù)結(jié)構(gòu)將增加代碼庫的整體文檔,并在與團(tuán)隊(duì)成員一起工作時(shí)改善我們自己的開發(fā)人員體驗(yàn)相同的代碼庫。
推薦閱讀
學(xué)習(xí)更多技能
請點(diǎn)擊下方公眾號
![]()

