深入探讨

声明文件理论:深入探讨

🌐 Declaration File Theory: A Deep Dive

将模块结构化以提供所需的确切 API 形式可能很棘手。例如,我们可能希望一个模块可以在有或没有 new 的情况下调用以生成不同类型,模块中暴露出各种命名类型的层次结构,并且模块对象上也有一些属性。

🌐 Structuring modules to give the exact API shape you want can be tricky. For example, we might want a module that can be invoked with or without new to produce different types, has a variety of named types exposed in a hierarchy, and has some properties on the module object as well.

通过阅读本指南,你将掌握编写复杂声明文件的工具,从而提供一个友好的 API 接口。本指南侧重于模块(或 UMD)库,因为这里的选项更为多样。

🌐 By reading this guide, you’ll have the tools to write complex declaration files that expose a friendly API surface. This guide focuses on module (or UMD) libraries because the options here are more varied.

关键概念

🌐 Key Concepts

通过理解 TypeScript 的一些关键概念,你可以完全理解如何声明任何形式的类型。

🌐 You can fully understand how to make any shape of declaration by understanding some key concepts of how TypeScript works.

类型

🌐 Types

如果你正在阅读这个指南,你可能已经大致了解 TypeScript 中的类型是什么。不过,更明确地说,类型 是通过以下方式引入的:

🌐 If you’re reading this guide, you probably already roughly know what a type in TypeScript is. To be more explicit, though, a type is introduced with:

  • 类型别名声明(type sn = number | string;
  • 接口声明 (interface I { x: number[]; })
  • 类声明 (class C { })
  • 一个枚举声明(enum E { A, B, C }
  • 一个引用某种类型的 import 声明

这些声明形式中的每一个都创建了一个新的类型名称。

🌐 Each of these declaration forms creates a new type name.

🌐 Values

与类型一样,你可能已经理解什么是值。值是在运行时我们可以在表达式中引用的名称。例如,let x = 5; 创建了一个名为 x 的值。

🌐 As with types, you probably already understand what a value is. Values are runtime names that we can reference in expressions. For example let x = 5; creates a value called x.

同样,明确地说,以下内容会创造值:

🌐 Again, being explicit, the following things create values:

  • letconstvar 声明
  • 包含值的 namespacemodule 声明
  • 一个 enum 声明
  • 一个 class 声明
  • 一个引用某个值的 import 声明
  • 一个 function 声明

命名空间

🌐 Namespaces

类型可以存在于_命名空间_中。例如,如果我们有声明let x: A.B.C,我们会说类型C来自A.B命名空间。

🌐 Types can exist in namespaces. For example, if we have the declaration let x: A.B.C, we say that the type C comes from the A.B namespace.

这种区别既微妙又重要——在这里,A.B 不一定是类型或数值。

🌐 This distinction is subtle and important — here, A.B is not necessarily a type or a value.

简单组合:一个名字,多重含义

🌐 Simple Combinations: One name, multiple meanings

给定一个名称 A,我们可能会为 A 找到最多三种不同的含义:一种类型、一个值或一个命名空间。名称的解释取决于其使用的上下文。例如,在声明 let m: A.A = A; 中,A 首先被用作命名空间,然后作为类型名,最后作为一个值。这些含义最终可能指向完全不同的声明!

🌐 Given a name A, we might find up to three different meanings for A: a type, a value or a namespace. How the name is interpreted depends on the context in which it is used. For example, in the declaration let m: A.A = A;, A is used first as a namespace, then as a type name, then as a value. These meanings might end up referring to entirely different declarations!

这可能看起来有些混乱,但只要我们不过度负载东西,它实际上非常方便。让我们来看一下这种组合行为的一些有用方面。

🌐 This may seem confusing, but it’s actually very convenient as long as we don’t excessively overload things. Let’s look at some useful aspects of this combining behavior.

内置组合

🌐 Built-in Combinations

机敏的读者会注意到,例如,class 出现在 typevalue 列表中。 声明 class C { } 创建了两样东西: 一个 type C,它指的是类的实例形状, 以及一个 value C,它指的是类的构造函数。 枚举声明的行为类似。

🌐 Astute readers will notice that, for example, class appeared in both the type and value lists. The declaration class C { } creates two things: a type C which refers to the instance shape of the class, and a value C which refers to the constructor function of the class. Enum declarations behave similarly.

用户组合

🌐 User Combinations

假设我们编写了一个模块文件 foo.d.ts

🌐 Let’s say we wrote a module file foo.d.ts:

ts
export var SomeVar: { a: SomeType };
export interface SomeType {
count: number;
}

然后引用它:

🌐 Then consumed it:

ts
import * as foo from "./foo";
let x: foo.SomeType = foo.SomeVar.a;
console.log(x.count);

这已经足够有效,但我们可以想象 SomeTypeSomeVar 关系非常密切,以至于你希望它们使用相同的名称。我们可以使用组合来将这两个不同的对象(值和类型)以相同的名称 Bar 呈现:

🌐 This works well enough, but we might imagine that SomeType and SomeVar were very closely related such that you’d like them to have the same name. We can use combining to present these two different objects (the value and the type) under the same name Bar:

ts
export var Bar: { a: Bar };
export interface Bar {
count: number;
}

这为引用代码中的解构提供了一个很好的机会:

🌐 This presents a very good opportunity for destructuring in the consuming code:

ts
import { Bar } from "./foo";
let x: Bar = Bar.a;
console.log(x.count);

同样,这里我们将 Bar 用作类型和数值。 请注意,我们不必将 Bar 声明为 Bar 类型的值 —— 它们是独立的。

🌐 Again, we’ve used Bar as both a type and a value here. Note that we didn’t have to declare the Bar value as being of the Bar type — they’re independent.

高级组合

🌐 Advanced Combinations

某些类型的声明可以在多个声明中合并。例如,class C { }interface C { } 可以共存,并且都为 C 类型提供属性。

🌐 Some kinds of declarations can be combined across multiple declarations. For example, class C { } and interface C { } can co-exist and both contribute properties to the C types.

只要不产生冲突,这是合法的。一个通用的经验法则是:值总是会与同名的其他值冲突,除非它们被声明为 namespace;类型如果通过类型别名声明(type s = string)也会产生冲突;而命名空间则永远不会冲突。

🌐 This is legal as long as it does not create a conflict. A general rule of thumb is that values always conflict with other values of the same name unless they are declared as namespaces, types will conflict if they are declared with a type alias declaration (type s = string), and namespaces never conflict.

让我们看看如何使用它。

🌐 Let’s see how this can be used.

添加使用 interface

🌐 Adding using an interface

我们可以通过另一个 interface 声明向 interface 添加额外的成员:

🌐 We can add additional members to an interface with another interface declaration:

ts
interface Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

这也适用于类:

🌐 This also works with classes:

ts
class Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

请注意,我们不能使用接口来扩展类型别名(type s = string;)。

🌐 Note that we cannot add to type aliases (type s = string;) using an interface.

添加使用 namespace

🌐 Adding using a namespace

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

🌐 A namespace declaration can be used to add new types, values, and namespaces in any way which does not create a conflict.

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

🌐 For example, we can add a static member to a class:

ts
class C {}
// ... elsewhere ...
namespace C {
export let x: number;
}
let y = C.x; // OK

请注意,在此示例中,我们向 C静态 端(其构造函数)添加了一个值。这是因为我们添加的是一个 ,而所有值的容器本身也是一个值(类型被命名空间所包含,而命名空间又被其他命名空间所包含)。

🌐 Note that in this example, we added a value to the static side of C (its constructor function). This is because we added a value, and the container for all values is another value (types are contained by namespaces, and namespaces are contained by other namespaces).

我们还可以将命名空间类型添加到类中:

🌐 We could also add a namespaced type to a class:

ts
class C {}
// ... elsewhere ...
namespace C {
export interface D {}
}
let y: C.D; // OK

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

🌐 In this example, there wasn’t a namespace C until we wrote the namespace declaration for it. The meaning C as a namespace doesn’t conflict with the value or type meanings of C created by the class.

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

🌐 Finally, we could perform many different merges using namespace declarations. This isn’t a particularly realistic example, but shows all sorts of interesting behavior:

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;

在此示例中,第一个块创建以下名称含义:

🌐 In this example, the first block creates the following name meanings:

  • 一个值 X(因为 namespace 声明包含一个值,Z
  • 命名空间 X(因为 namespace 声明包含一个类型 Y
  • X 命名空间中的 Y 类型
  • X 命名空间中的类型 Z(类的实例形状)
  • ZX 值(类的构造函数)的一个属性

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

🌐 The second block creates the following name meanings:

  • 一个值 Y(类型为 number),它是 X 值的一个属性
  • 一个命名空间 Z
  • 一个属于 X 值的 Z 属性值
  • X.Z 命名空间中的类型 C
  • C 的值是 X.Z 值的一个属性
  • 一种类型 X