深入探讨

声明文件理论:深入探讨

构建模块以获得您想要的精确 API 形状可能很棘手。例如,我们可能想要一个可以带或不带new调用以生成不同类型的模块,它在层次结构中公开各种命名类型,并且在模块对象上也有一些属性。

通过阅读本指南,您将拥有编写复杂声明文件以公开友好 API 表面的工具。本指南侧重于模块(或 UMD)库,因为这里的选择更多样化。

关键概念

通过了解 TypeScript 工作方式的一些关键概念,您可以完全理解如何创建任何形状的声明。

类型

如果您正在阅读本指南,您可能已经大致了解 TypeScript 中的类型是什么。不过,为了更明确起见,类型是通过以下方式引入的:

  • 类型别名声明 (type sn = number | string;)
  • 接口声明 (interface I { x: number[]; })
  • 类声明 (class C { })
  • 枚举声明 (enum E { A, B, C })
  • 引用类型的import声明

这些声明形式中的每一个都创建一个新的类型名称。

与类型类似,您可能已经理解了什么是值。值是在表达式中可以引用的运行时名称。例如,let x = 5; 创建了一个名为 x 的值。

同样,明确地说,以下内容会创建值

  • letconstvar 声明
  • 包含值的 namespacemodule 声明
  • enum 声明
  • class 声明
  • 引用值的 import 声明
  • function 声明

命名空间

类型可以存在于 命名空间 中。例如,如果我们有声明 let x: A.B.C,我们说类型 C 来自 A.B 命名空间。

这种区别很微妙也很重要——这里,A.B 不一定是类型或值。

简单组合:一个名称,多种含义

给定一个名称 A,我们可能会找到 A 的三种不同的含义:类型、值或命名空间。名称的解释取决于它使用的上下文。例如,在声明 let m: A.A = A; 中,A 首先用作命名空间,然后用作类型名称,最后用作值。这些含义最终可能引用完全不同的声明!

这可能看起来很混乱,但实际上非常方便,只要我们不过度使用它。让我们看看这种组合行为的一些有用方面。

内置组合

敏锐的读者会注意到,例如,class 同时出现在了 类型 列表中。声明 class C { } 创建了两个东西:一个 类型 C,它指的是类的实例形状,以及一个 C,它指的是类的构造函数。枚举声明的行为类似。

用户组合

假设我们编写了一个模块文件 foo.d.ts

ts
export var SomeVar: { a: SomeType };
export interface SomeType {
count: number;
}

然后使用它

ts
import * as foo from "./foo";
let x: foo.SomeType = foo.SomeVar.a;
console.log(x.count);

这已经足够好了,但我们可以想象 SomeTypeSomeVar 关系非常密切,以至于你希望它们具有相同的名称。我们可以使用组合来在同一个名称 Bar 下呈现这两个不同的对象(值和类型)

ts
export var Bar: { a: Bar };
export interface Bar {
count: number;
}

这为在使用代码中解构提供了非常好的机会

ts
import { Bar } from "./foo";
let x: Bar = Bar.a;
console.log(x.count);

同样,我们在这里将 Bar 同时用作类型和值。请注意,我们不必将 Bar 值声明为 Bar 类型 - 它们是独立的。

高级组合

某些类型的声明可以在多个声明之间组合。例如,class C { }interface C { } 可以共存,并且都为 C 类型贡献属性。

只要不产生冲突,这都是合法的。一个经验法则是,值总是与其他同名值冲突,除非它们被声明为namespace;类型如果使用类型别名声明(type s = string)则会冲突;而命名空间永远不会冲突。

让我们看看如何使用它。

使用interface添加

我们可以使用另一个interface声明向interface添加额外的成员。

ts
interface Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

这同样适用于类。

ts
class Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

注意,我们不能使用接口向类型别名(type s = string;)添加内容。

使用namespace添加

namespace声明可以用来以任何不产生冲突的方式添加新的类型、值和命名空间。

例如,我们可以向类添加一个静态成员。

ts
class C {}
// ... elsewhere ...
namespace C {
export let x: number;
}
let y = C.x; // OK

注意,在这个例子中,我们向C静态部分(它的构造函数)添加了一个值。这是因为我们添加了一个,而所有值的容器是另一个值(类型包含在命名空间中,命名空间包含在其他命名空间中)。

我们也可以向类添加一个命名空间类型。

ts
class C {}
// ... elsewhere ...
namespace C {
export interface D {}
}
let y: C.D; // OK

在这个例子中,直到我们为它编写namespace声明之前,才存在命名空间CC作为命名空间的含义不会与类创建的C的值或类型的含义冲突。

最后,我们可以使用namespace声明执行许多不同的合并。这不是一个特别现实的例子,但展示了各种有趣的行为。

ts
namespace X {
export interface Y {}
export class Z {}
}
// ... elsewhere ...
namespace X {
export var Y: number;
export namespace Z {
export class C {}
}
}
type X = string;

在这个例子中,第一个代码块创建了以下名称含义:

  • 一个值X(因为namespace声明包含一个值Z
  • 一个命名空间X(因为namespace声明包含一个类型Y
  • X命名空间中的一个类型Y
  • X命名空间中的一个类型Z(类的实例形状)
  • 一个值Z,它是X值的属性(类的构造函数)

第二个代码块创建了以下名称含义:

  • 一个值Y(类型为number),它是X值的属性
  • 一个命名空间Z
  • 一个值Z,它是X值的属性
  • X.Z命名空间中的一个类型C
  • 一个值C,它是X.Z值的属性
  • 一个类型X

TypeScript 文档是一个开源项目。帮助我们改进这些页面 通过发送拉取请求

此页面的贡献者
MHMohamed Hegazy (54)
OTOrta Therox (12)
1+

上次更新:2024 年 3 月 21 日