close

英文 | https://www.digitalocean.com/community/tutorials/how-to-use-namespaces-in-typescript

翻譯 | 楊小愛

介紹
TypeScript 是 JavaScript 語言的擴展,它使用 JavaScript 運行時和編譯時類型檢查器。
TypeScript 提供了多種方法來表示代碼中的對象,其中一種是使用接口。 TypeScript 中的接口有兩種使用場景:您可以創建類必須遵循的約定,例如,這些類必須實現的成員,還可以在應用程序中表示類型,就像普通的類型聲明一樣。
您可能會注意到接口和類型共享一組相似的功能。
事實上,一個幾乎總是可以替代另一個。
主要區別在於接口可能對同一個接口有多個聲明,TypeScript 將合併這些聲明,而類型只能聲明一次。您還可以使用類型來創建原始類型(例如字符串和布爾值)的別名,這是接口無法做到的。
TypeScript 中的接口是表示類型結構的強大方法。它們允許您以類型安全的方式使用這些結構並同時記錄它們,從而直接改善開發人員體驗。
在今天的文章中,我們將在 TypeScript 中創建接口,學習如何使用它們,並了解普通類型和接口之間的區別。
我們將嘗試不同的代碼示例,可以在TypeScript 環境或 TypeScript Playground(一個允許您直接在瀏覽器中編寫 TypeScript 的在線環境)中遵循這些示例。
準備工作
要完成今天的示例,我們將需要做如下準備工作:

一個環境。我們可以執行 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 網站。

如果你不想在本地機器上創建 TypeScript 環境,你可以使用官方的 TypeScript Playground 來跟隨。

您將需要足夠的 JavaScript 知識,尤其是 ES6+ 語法,例如解構、rest 運算符和導入/導出。如果您需要有關這些主題的更多信息,建議閱讀我們的如何用 JavaScript 編寫代碼系列。

本文教程將參考支持 TypeScript 並顯示內聯錯誤的文本編輯器的各個方面。這不是使用 TypeScript 所必需的,但確實可以更多地利用 TypeScript 功能。為了獲得這些好處,您可以使用像 Visual Studio Code 這樣的文本編輯器,它完全支持開箱即用的 TypeScript。你也可以在 TypeScript Playground 中嘗試這些好處。

本教程中顯示的所有示例都是使用 TypeScript 4.2.2 版創建的。

在 TypeScript 中創建命名空間

在本節中,我們將一起來學習在 TypeScript 中創建命名空間以說明一般語法。
要創建命名空間,我們將使用命名空間關鍵字,後跟命名空間的名稱,然後是 {} 塊。
例如,我們將創建一個 DatabaseEntity 命名空間來保存數據庫實體,就像我們使用對象關係映射 (ORM) 庫一樣。
將以下代碼添加到新的 TypeScript 文件中:
namespace DatabaseEntity {}

這聲明了 DatabaseEntity 命名空間,但尚未向該命名空間添加代碼。 接下來,在命名空間中添加一個 User 類來表示數據庫中的一個 User 實體:

namespace DatabaseEntity { class User { constructor(public name: string) {} }}

我們可以在命名空間中正常使用 User 類。 為了說明這一點,創建一個新的 User 實例並將其存儲在 newUser 變量中:

namespace DatabaseEntity { class User { constructor(public name: string) {} } const newUser = new User("Jon");}

這是有效的代碼。

但是,如果我們嘗試在命名空間之外使用 User,TypeScript 編譯器會給我們返回錯誤 2339:

OutputProperty 'User' does not exist on type 'typeof DatabaseEntity'. (2339)

如果我們想在命名空間之外使用類,則必須首先導出 User 類以在外部可用,如下面突出顯示的代碼所示:

namespace DatabaseEntity { exportclass User { constructor(public name: string) {} } const newUser = new User("Jon");}

我們現在可以使用完全限定名稱訪問 DatabaseEntity 命名空間之外的 User 類。 在這種情況下,完全限定名稱是 DatabaseEntity.User:

namespace DatabaseEntity { export class User { constructor(public name: string) {} } const newUser = new User("Jon");}const newUserOutsideNamespace = new DatabaseEntity.User("Jane");

我們可以從命名空間中導出任何內容,包括變量,然後這些變量將成為命名空間中的屬性。

在以下代碼中,我們將導出 newUser 變量:

namespace DatabaseEntity { export class User { constructor(public name: string) {} } exportconst newUser = new User("Jon");}console.log(DatabaseEntity.newUser.name);

由於變量 newUser 已導出,因此,我們可以將其作為命名空間的屬性進行訪問。 運行此代碼會將以下內容打印到控制台:

OutputJon

就像接口一樣,TypeScript 中的命名空間也允許聲明合併。 這意味着同一命名空間的多個聲明將合併為一個聲明。 如果我們需要稍後在代碼中擴展命名空間,這可以增加命名空間的靈活性。

使用前面的示例,這意味着如果我們再次聲明 DatabaseEntity 命名空間,我們將能夠使用更多屬性擴展命名空間。 使用另一個命名空間聲明將一個新類 UserRole 添加到我們的 DatabaseEntity 命名空間:

namespace DatabaseEntity { export class User { constructor(public name: string) {} } export const newUser = new User("Jon");}namespace DatabaseEntity { export class UserRole { constructor(public user: User, public role: string) {} } export const newUserRole = new UserRole(newUser, "admin");}

在新的 DatabaseEntity 命名空間聲明中,我們可以使用以前在 DatabaseEntity 命名空間中導出的任何成員,包括從以前的聲明中導出的成員,而不必使用它們的完全限定名。

我們正在使用在第一個命名空間中聲明的名稱來將 UserRole 構造函數中的用戶參數的類型設置為 User 類型,並在使用 newUser 值創建新的 UserRole 實例時。這僅是可能的,因為,我們在之前的命名空間聲明中導出了這些內容。

現在,我們已經了解了命名空間的基本語法,我們可以繼續研究 TypeScript 編譯器如何將命名空間轉換為 JavaScript。

檢查使用命名空間時生成的 JavaScript 代碼

TypeScript 中的命名空間不僅僅是一個編譯時特性。他們還更改了生成的 JavaScript 代碼。要了解有關命名空間如何工作的更多信息,我們可以分析支持此 TypeScript 功能的 JavaScript。

在這一步中,我們將獲取上一節中的代碼片段並檢查它們的底層 JavaScript 實現。

以我們在第一個示例中使用的代碼為例:

namespace DatabaseEntity { export class User { constructor(public name: string) {} } export const newUser = new User("Jon");}console.log(DatabaseEntity.newUser.name);

TypeScript 編譯器會為此 TypeScript 片段生成以下 JavaScript 代碼:

"use strict";var DatabaseEntity;(function (DatabaseEntity) { class User { constructor(name) { this.name = name; } } DatabaseEntity.User = User; DatabaseEntity.newUser = new User("Jon");})(DatabaseEntity || (DatabaseEntity = {}));console.log(DatabaseEntity.newUser.name);

為了聲明 DatabaseEntity 命名空間,TypeScript 編譯器創建一個名為 DatabaseEntity 的未初始化變量,然後,創建一個立即調用函數表達式 (IIFE)。 此 IIFE 接收單個參數 DatabaseEntity || (DatabaseEntity = {}),這是 DatabaseEntity 變量的當前值。 如果未設置為真值,則將變量的值設置為空對象。

在將 DatabaseEntity 的值傳遞給 IIFE 時將其設置為空值是可行的,因為賦值操作的返回值是被賦值的值。 在這種情況下,這是空對象。

在 IIFE 內部,創建了 User 類,然後,將其分配給 DatabaseEntity 對象的 User 屬性。 newUser 屬性也是如此,我們將屬性分配給新 User 實例的值。

現在看一下第二個代碼示例,其中有多個命名空間聲明:

namespace DatabaseEntity { export class User { constructor(public name: string) {} } export const newUser = new User("Jon");}namespace DatabaseEntity { export class UserRole { constructor(public user: User, public role: string) {} } export const newUserRole = new UserRole(newUser, "admin");}

生成的 JavaScript 代碼如下所示:

"use strict";var DatabaseEntity;(function (DatabaseEntity) { class User { constructor(name) { this.name = name; } } DatabaseEntity.User = User; DatabaseEntity.newUser = new User("Jon");})(DatabaseEntity || (DatabaseEntity = {}));(function (DatabaseEntity) { class UserRole { constructor(user, role) { this.user = user; this.role = role; } } DatabaseEntity.UserRole = UserRole; DatabaseEntity.newUserRole = new UserRole(DatabaseEntity.newUser, "admin");})(DatabaseEntity || (DatabaseEntity = {}));

代碼的開頭看起來與之前的相同,未初始化的變量 DatabaseEntity,然後是一個 IIFE,其中實際代碼設置了 DatabaseEntity 對象的屬性。這一次,雖然,還有另一個 IIFE。這個新的 IIFE 與 DatabaseEntity 命名空間的第二個聲明相匹配。

現在,當執行第二個 IIFE 時,DatabaseEntity 已經綁定到一個對象,因此,我們只是通過添加額外屬性來擴展已經可用的對象。

我們現在已經了解了 TypeScript 命名空間的語法以及它們在底層 JavaScript 中的工作方式。有了這個上下文,我們現在可以運行命名空間的一個常見用例:為外部庫定義類型而無需鍵入。

使用命名空間為外部庫提供類型

在這部分內容中,我們將體驗命名空間有用的場景之一:為外部庫創建模塊聲明。為此,我們將在 TypeScript 項目中編寫一個新文件來聲明類型,然後更改 tsconfig.json 文件以使 TypeScript 編譯器識別類型。

注意:要執行後續步驟,需要一個可以訪問文件系統的 TypeScript 環境。如果您使用的是 TypeScript Playground,則可以通過單擊頂部菜單中的導出,然後在 CodeSandbox 中打開,將現有代碼導出到 CodeSandbox 項目。這將允許您創建新文件並編輯 tsconfig.json 文件。

並非 npm 註冊表中的每個可用包都捆綁了自己的 TypeScript 模塊聲明。這意味着在項目中安裝包時,您可能會遇到與包缺少類型聲明相關的編譯錯誤,或者必須使用所有類型都設置為 any 的庫。根據您使用 TypeScript 的嚴格程度,這可能是一個不希望的結果。

希望這個包將有一個由 DefinetelyTyped 社區創建的 @types 包,允許您安裝包並獲得該庫的工作類型。

但是,情況並非總是如此,有時您必須處理一個不捆綁其自己的類型模塊聲明的庫。在這種情況下,如果您想保持您的代碼完全類型安全,您必須自己創建模塊聲明。

例如,假設您正在使用一個名為 example-vector3 的向量庫,它使用單個方法 add 導出單個類 Vector3。此方法用於將兩個 Vector3 向量相加。

庫中的代碼可能如下所示:

export class Vector3 { super(x, y, z) { this.x = x; this.y = y; this.z = z; } add(vec) { let x = this.x + vector.x; let y = this.y + vector.y; let z = this.z + vector.z; let newVector = new Vector3(x, y, z); return newVector }}

這導出了一個類,該類創建具有 x、y 和 z 屬性的向量,用於表示向量的坐標分量。

接下來,看一下使用假設庫的示例代碼:

index.ts

import { Vector3 } from "example-vector3";const v1 = new Vector3(1, 2, 3);const v2 = new Vector3(1, 2, 3);const v3 = v1.add(v2);

example-vector3 庫沒有與它自己的類型聲明捆綁在一起,因此, TypeScript 編譯器將給出錯誤 2307:

OutputCannot find module 'example-vector3' or its corresponding type declarations. ts(2307)

為了解決這個問題,我們現在將為這個包創建一個類型聲明文件。

首先,創建一個名為 types/example-vector3/index.d.ts 的新文件。

然後,在您喜歡的編輯器中打開它。

在此文件中寫入以下代碼:

declare module "example-vector3" { export = vector3; namespace vector3 { }}

在此代碼中,我們正在為 example-vector3 模塊創建類型聲明。 代碼的第一部分是聲明模塊塊本身。 TypeScript 編譯器將解析這個塊並解釋其中的所有內容,就好像它是模塊本身的類型表示一樣。 這意味着我們在此處聲明的任何內容,TypeScript 都將用於推斷模塊的類型。

現在,您說這個模塊導出了一個名為 vector3 的命名空間,該命名空間目前是空的。

保存並退出此文件。

TypeScript 編譯器當前不知道您的聲明文件,因此您必須將其包含在您的 tsconfig.json 中。

為此,通過將 types 屬性添加到 compilerOptions 選項來編輯項目 tsconfig.json:

{ "compilerOptions": { ... "types": ["./types/example-vector3/index.d.ts"] }}

現在,如果我們返回原始代碼,我們將看到錯誤已更改。TypeScript 編譯器現在給出錯誤是2305:

OutputModule '"example-vector3"' has no exported member 'Vector3'. ts(2305)

當我們為 example-vector3 創建模塊聲明時,導出當前設置為空命名空間。 沒有從該命名空間中導出 Vector3 類。

重新打開 types/example-vector3/index.d.ts 並編寫以下代碼:

declare module "example-vector3" { export = vector3; namespace vector3 { export class Vector3 { constructor(x: number, y: number, z: number); add(vec: Vector3): Vector3; } }}

在此代碼中,請注意,我們現在如何在 vector3 命名空間內導出一個類。模塊聲明的主要目標是提供由庫公開的值的類型信息。這樣,我們可以以類型安全的方式使用它。

在這種情況下,我們知道 example-vector3 庫提供了一個名為 Vector3 的類,該類在構造函數中接受三個數字,並且具有用於將兩個 Vector3 實例相加的 add 方法,並返回一個新實例作為結果。

我們無需在此處提供實現,只需提供類型信息本身。不提供實現的聲明在 TypeScript 中稱為環境聲明,通常在 .d.ts 文件中創建這些聲明。

此代碼現在將正確編譯並具有 Vector3 類的正確類型。

使用命名空間,我們可以將庫導出的內容隔離到單個類型單元中,在本例中為 vector3 命名空間。這使得自定義模塊聲明變得更加容易,甚至可以通過將類型聲明提交到 DefinetelyTyped 存儲庫來使所有開發人員都可以使用它。

最後結論

在今天的教程中,我們了解了 TypeScript 中命名空間的基本語法,並檢查了 TypeScript 編譯器將其更改為的 JavaScript。

我們還嘗試了命名空間的一個常見用例:為尚未鍵入的外部庫提供環境類型。

雖然,不推薦使用命名空間,但並不總是建議在代碼庫中使用命名空間作為代碼組織機制。現代代碼應該使用 ES 模塊語法,因為它具有命名空間提供的所有功能,並且從 ECMAScript 2015 開始,它成為規範的一部分。

但是,在創建模塊聲明時,仍然建議使用命名空間,因為它允許更簡潔的類型聲明。

如果你還想閱讀更多有關 TypeScript 的教程文章,請看下面的推薦閱讀內容,如果你覺得我今天的教程不錯,請點讚我,關注我,並將這篇文章分享給你的朋友,也許能夠幫助到他。

最後,感謝你的閱讀,編程快樂!

推薦閱讀

如何在 TypeScript 中使用接口
如何在 TypeScript 中使用裝飾器
如何在 TypeScript 中使用類
如何在 TypeScript 中使用Enum(枚舉)
如何在 TypeScript 中使用函數
如何在 TypeScript 中創建自定義類型
如何在TypeScript中使用基本類型

學習更多技能

請點擊下方公眾號


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()