Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

深入探究

声明文件理论:深入探究

构建模块以提供所需的精确 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;
}
// ... 其他地方 ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // 可以

这也适用于类:

ts
class Foo {
  x: number;
}
// ... 其他地方 ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // 可以

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

使用 namespace 添加

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

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

ts
class C {}
// ... 其他地方 ...
namespace C {
  export let x: number;
}
let y = C.x; // 可以

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

我们还可以向类添加一个命名空间类型:

ts
class C {}
// ... 其他地方 ...
namespace C {
  export interface D {}
}
let y: C.D; // 可以

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

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

ts
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
  • 一个类型 YX 命名空间中
  • 一个类型 ZX 命名空间中(类的实例形状)
  • 一个值 ZX 值的一个属性(类的构造函数)

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

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