深入探讨

声明文件原理:深入探讨

¥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;)

    ¥A type alias declaration (type sn = number | string;)

  • 接口声明(interface I { x: number[]; }

    ¥An interface declaration (interface I { x: number[]; })

  • 类声明(class C { }

    ¥A class declaration (class C { })

  • 枚举声明 (enum E { A, B, C })

    ¥An enum declaration (enum E { A, B, C })

  • 引用类型的 import 声明

    ¥An import declaration which refers to a type

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

¥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 声明

    ¥let, const, and var declarations

  • 包含值的 namespacemodule 声明

    ¥A namespace or module declaration which contains a value

  • enum 声明

    ¥An enum declaration

  • class 声明

    ¥A class declaration

  • 引用值的 import 声明

    ¥An import declaration which refers to a value

  • function 声明

    ¥A function declaration

命名空间

¥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 同时出现在类型和值列表中。声明 class C { } 创建了两件事:类型 C 指的是类的实例形状,值 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

    ¥A value X (because the namespace declaration contains a value, Z)

  • 命名空间 X(因为 namespace 声明包含类型 Y

    ¥A namespace X (because the namespace declaration contains a type, Y)

  • X 命名空间中的类型 Y

    ¥A type Y in the X namespace

  • X 命名空间中的类型 Z(类的实例形态)

    ¥A type Z in the X namespace (the instance shape of the class)

  • 作为 X 值(类的构造函数)的属性的值 Z

    ¥A value Z that is a property of the X value (the constructor function of the class)

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

¥The second block creates the following name meanings:

  • 作为 X 值属性的值 Y(类型为 number

    ¥A value Y (of type number) that is a property of the X value

  • 命名空间 Z

    ¥A namespace Z

  • 作为 X 值属性的值 Z

    ¥A value Z that is a property of the X value

  • X.Z 命名空间中的类型 C

    ¥A type C in the X.Z namespace

  • 作为 X.Z 值属性的值 C

    ¥A value C that is a property of the X.Z value

  • 一个类型 X

    ¥A type X