声明文件理论:深入探讨
构建模块以获得您想要的精确 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
的值。
同样,明确地说,以下内容会创建值
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
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);
这已经足够好了,但我们可以想象 SomeType
和 SomeVar
关系非常密切,以至于你希望它们具有相同的名称。我们可以使用组合来在同一个名称 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
(类的实例形状)- 一个值
Z
,它是X
值的属性(类的构造函数)
第二个代码块创建了以下名称含义:
- 一个值
Y
(类型为number
),它是X
值的属性 - 一个命名空间
Z
- 一个值
Z
,它是X
值的属性 X.Z
命名空间中的一个类型C
- 一个值
C
,它是X.Z
值的属性 - 一个类型
X