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