深入探讨

声明文件理论:深度解析

构建模块以提供你所期望的确切 API 形态可能很棘手。例如,我们可能需要一个既可以配合 new 调用又可以不配合 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 声明之前,并没有命名空间 C。作为命名空间的 C 的含义与类创建的 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(类的实例形态)
  • 作为 X 值属性的值 Z(类的构造函数)

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

  • 作为 X 值属性的值 Y(类型为 number
  • 命名空间 Z
  • 作为 X 值属性的值 Z
  • X.Z 命名空间中的类型 C
  • 作为 X.Z 值属性的值 C
  • 类型 X

TypeScript 文档是一个开源项目。请发送 Pull Request 帮助我们改进这些页面 ❤

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

最后更新:2026 年 3 月 27 日