通过结构化模块,为所需的 API 提供精确的原型并不是一件容易的事情。例如:我们可能需要一个模块在被调用时,带上 new
关键字与不带 new
关键字来生成不同的类型,在层级关系中,暴露出不同的命名,并且还在模块对象上具有一些属性。
通过阅读本指南,您将拥有编写复杂定义文件的工具,这些文件将提供友好的 API 表面。 本指南关注模块(或 UMD)库,因为这里的选项更多。
一、关键概念(Key Concepts)
通过理解 TypeScript 的一些关键概念,您可以充分理解如何进行任何形式的定义。
1、类型(Types)
如果您正在阅读本指南,您可能已经大致了解 TypeScript 中的类型。 然而,再明确一下,一种类型可以通过以下形式被引入:
- 类型别名声明:
type sn = number | string;
- 接口声明:
interface I { x: number[]; }
- 类声明:
class C { }
- 枚举声明:
enum E { A, B, C }
-
import
声明指向一个类型
2、值(Values)
与类型一样,您可能已经理解了什么是 Value。 Value 是我们可以在表达式中引用的运行时名称。 例如:let x = 5;
创建一个名为 x
的 Value。
明确一下通过以下形式创建 Value:
-
let
、const
、var
声明 -
namespace
或module
声明包含一个 Value -
enum
声明 -
class
声明 -
import
声明指向一个值 -
function
声明
3、命名空间(Namespaces)
类型可以存在于命名空间中。例如:有 let x: A.B.C
声明,我们说类型 C
来自命名空间 A.B
。
这种区别是微妙而重要的——在这里,A.B
不一定是必要的类型或值。
二、简单的组合:一个名字,多重含义
给定一个名称 A
,我们可以为 A
找到三种不同的含义:一个类型,一个值或一个命名空间。 名称的解释方式取决于其使用的上下文。 例如,在声明 let m:A.A = A;
中,A
首先用作命名空间,然后用作类型名称,然后用作值。 这些含义最终可能指的是完全不同的声明!
这看起来可能会让人困惑,但只要我们不过分使用,它实际上非常方便。 我们来看看这种组合行为的一些有用的方面。
1、內建组合(Built-in Combinations)
精明的读者会注意到,例如:class
同时出现在 type 和 value 清单中。 声明 class C {}
创建了两项内容:
- 类型
C
——类C
的实例原型 - 值
C
——类C
的构造函数
枚举声明的行为类似。
2、用户组合(User Combinations)
假设我们写了一个模块文件 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
类型——它们是独立的。
三、高级组合(Advanced Combinations)
某些类型的声明可以在多个声明中组合使用。 例如,class C { }
和 interface C { }
可以共存,并且都为 C
类型提供属性。
只要不产生冲突,这是合法的。 一般的经验法则是:
- 值总是与其他相同名称的值冲突,除非它们被声明为命名空间;
- 如果使用类型别名进行声明类型,如:
type s = string
,则可能会产生冲突; - 命名空间永不冲突。
我们来看看如何使用它。
1、使用 interface
添加(Adding using an interface
)
我们可以使用另一个 interface
声明将其他成员添加到现有 interface
声明中:
interface Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK
这也适用于类:
class Foo {
x: number;
}
// ... elsewhere ...
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK
请注意,我们不能使用 interface
添加类型别名,如:type s = string;
。
2、使用 namespace
添加(Adding using a namespace
)
namespace
声明可用于以任何不会产生冲突的方式,添加新的类型、值和命名空间。
例如,为类添加静态成员:
class C {
}
// ... elsewhere ...
namespace C {
export let x: number;
}
let y = C.x; // OK
请注意,在这个例子中,我们向 C
的静态端(它的构造函数)添加了一个值。 这是因为我们添加了一个值,并且所有值的容器都是另一个值(类型由命名空间包含,命名空间由其他命名空间包含)。
我们也可以为类添加一个命名空间类型:
class C {
}
// ... elsewhere ...
namespace C {
export interface D { }
}
let y: C.D; // OK
在这个例子中,直到我们写了命名空间声明之前,没有一个命名空间 C
。C
作为命名空间的含义,与该类创建的 C
的值或类型的含义不冲突。
最后,我们可以使用命名空间声明来执行许多不同的合并。 这不是一个特别实际的例子,但显示了各种有趣的行为:
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
) - 类型
Y
在命名空间X
中 - 类型
Z
在命名空间X
中(类的实例原型) - 值
Z
,属于X
值的属性(类的构造器)
第二个块创建以下名称含义:
- 值
Y
(number
类型,X
值的属性) - 命名空间
Z
- 值
Z
(X
值的属性) - 类型
C
(在命名空间X.Z
中) - 值
C
(X.Z
值的属性) - 类型
X
四、使用 export =
或 import
一个重要的规则,是 export
和 import
声明输出或输入它们目标的所有含义。
五、参考资料
译自 Definition File Theory: A Deep Dive
(完)