• 如何在 TypeScript 中使用命名空间
  • 发布于 2个月前
  • 174 热度
    0 评论
介绍

TypeScript 是 JavaScript 语言的扩展,它使用 JavaScript 运行时和编译时类型检查器。TypeScript 提供了多种方法来表示代码中的对象,其中一种是使用接口。 TypeScript 中的接口有两种使用场景:您可以创建类必须遵循的约定,例如,这些类必须实现的成员,还可以在应用程序中表示类型,就像普通的类型声明一样。 


您可能会注意到接口和类型共享一组相似的功能。事实上,一个几乎总是可以替代另一个。主要区别在于接口可能对同一个接口有多个声明,TypeScript 将合并这些声明,而类型只能声明一次。您还可以使用类型来创建原始类型(例如字符串和布尔值)的别名,这是接口无法做到的。
TypeScript 中的接口是表示类型结构的强大方法。它们允许您以类型安全的方式使用这些结构并同时记录它们,从而直接改善开发人员体验。

在今天的文章中,我们将在 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),这也适用。

2.此外,我们需要在机器上安装 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:
Output
Property '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 已导出,因此,我们可以将其作为命名空间的属性进行访问。 运行此代码会将以下内容打印到控制台:
Output
Jon
就像接口一样,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:
Output
Cannot 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:
Output
Module '"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 的教程文章,请看下面的推荐阅读内容,如果你觉得我今天的教程不错,请点赞我,关注我,并将这篇文章分享给你的朋友,也许能够帮助到他。
用户评论