声明文件理论:深度解析
构建模块以提供你所期望的确切 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 的值。
再次明确一下,以下事物会创建值:
let、const和var声明- 包含值的
namespace或module声明 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
tsexport var SomeVar: { a: SomeType };export interface SomeType {count: number;}
然后使用它
tsimport * as foo from "./foo";let x: foo.SomeType = foo.SomeVar.a;console.log(x.count);
这工作得很好,但我们可能想象 SomeType 和 SomeVar 关系非常紧密,以至于你希望它们具有相同的名称。我们可以使用组合将这两个不同的对象(值和类型)呈现为同一个名称 Bar
tsexport var Bar: { a: Bar };export interface Bar {count: number;}
这为使用代码中的解构提供了非常好的机会
tsimport { 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 添加额外的成员
tsinterface Foo {x: number;}// ... elsewhere ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
这也适用于类
tsclass Foo {x: number;}// ... elsewhere ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
请注意,我们不能使用接口向类型别名(type s = string;)添加内容。
使用 namespace 添加
namespace 声明可以用来以任何不产生冲突的方式添加新的类型、值和命名空间。
例如,我们可以向类添加静态成员
tsclass C {}// ... elsewhere ...namespace C {export let x: number;}let y = C.x; // OK
请注意,在此示例中,我们向 C 的静态侧(其构造函数)添加了一个值。这是因为我们添加了一个值,而所有值的容器是另一个值(类型由命名空间容纳,命名空间由其他命名空间容纳)。
我们还可以向类添加带命名空间的类型
tsclass C {}// ... elsewhere ...namespace C {export interface D {}}let y: C.D; // OK
在此示例中,在我们为其编写 namespace 声明之前,并没有命名空间 C。作为命名空间的 C 的含义与类创建的 C 的值或类型含义不冲突。
最后,我们可以使用 namespace 声明执行许多不同的合并。这并不是一个特别真实的示例,但展示了各种有趣的组合行为
tsnamespace 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命名空间中的类型YX命名空间中的类型Z(类的实例形态)- 作为
X值属性的值Z(类的构造函数)
第二个块创建了以下名称含义
- 作为
X值属性的值Y(类型为number) - 命名空间
Z - 作为
X值属性的值Z X.Z命名空间中的类型C- 作为
X.Z值属性的值C - 类型
X