深入

    阅读这篇指南后,你就会了解如何编写复杂的声明文件来提供友好的 API 。 这篇指南针对于模块(或UMD)代码库,因为它们的选择具有更高的可变性。

    如果你理解了一些关于 TypeScript 是如何工作的核心概念, 那么你就能够为任何结构书写声明文件。

    如果你正在阅读这篇指南,你可能已经大概了解 TypeScript 里的类型指是什么。 明确一下,类型通过以下方式引入:

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

    以上每种声明形式都会创建一个新的类型名称。

    与类型相比,你可能已经理解了什么是值。 值是运行时的名字,它可以在表达式里引用。 比如let x = 5;创建了一个名为x的值。

    同样地,以下方式能够创建值:

    • letconst,和var声明
    • 包含值的namespacemodule声明
    • enum声明
    • class声明
    • 指向值的import声明
    • function声明

    类型可以存在于命名空间里。 比如,有这样的声明let x: A.B.C, 我们就认为C类型来自于A.B命名空间。

    这个区别虽细微但很重要 — 这里,A.B不是必需的类型或值。

    一个给定的名字A,我们可以找出三种不同的意义:一个类型,一个值或一个命名空间。 要如何去解析这个名字要看它所在的上下文是怎样的。 比如,在声明let m: A.A = A;中,A首先被当做命名空间,然后做为类型名,最后是值。 这些意义最终可能会指向完全不同的声明!

    内置组合

    眼尖的读者可能会注意到,比如,class同时出现在类型列表里。 class C { }声明创建了两个东西: 类型C指向类的实例结构, C指向类构造函数。 枚举声明拥有相似的行为。

    假设我们写了模块文件foo.d.ts:

    这样使用它:

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

    这可以很好地工作,但是我们知道SomeTypeSomeVar密切相关 因此我们想让它们有相同的名字。 我们可以使用组合通过相同的名字Bar表示这两种不同的对象(值和对象):

    1. export interface Bar {
    2. count: number;

    这提供了使用解构的机会:

    再次地,这里我们使用Bar做为类型和值。 注意我们没有声明Bar值为Bar类型 — 它们是独立的。

    有一些声明能够通过多个声明组合。 比如,class C { }interface C { }可以同时存在并且都可以做为C类型的属性。

    只要不产生冲突就是合法的。 一个普通的规则是值总是会和同名的其它值产生冲突,除非它们在不同命名空间里,类型冲突则发生在使用类型别名声明的情况下(type s = string),命名空间永远不会发生冲突。

    让我们看看如何使用。

    通过interface添加

    1. interface Foo {
    2. x: number;
    3. }
    4. // ... elsewhere ...
    5. interface Foo {
    6. y: number;
    7. }
    8. let a: Foo = ...;
    9. console.log(a.x + a.y); // OK

    这同样作用于类:

    1. class Foo {
    2. x: number;
    3. }
    4. // ... elsewhere ...
    5. interface Foo {
    6. y: number;
    7. }
    8. let a: Foo = ...;
    9. console.log(a.x + a.y); // OK

    注意我们不能使用接口往类型别名里添加成员(type s = string;

    namespace声明可以用来添加新类型,值和命名空间,只要不出现冲突即可。

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

    注意在这个例子里,我们添加一个值到C静态部分(它的构造函数)。 这里因为我们添加了一个,且其它值的容器是另一个值(类型包含于命名空间,命名空间包含于另外的命名空间)。

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

    1. class C {}
    2. // ... elsewhere ...
    3. namespace C {
    4. export interface D {}
    5. let y: C.D; // OK

    在这个例子里,直到我们写了namespace声明才有了命名空间C。 做为命名空间的C不会与类创建的值或类型C相互冲突。

    最后,我们可以进行不同的合并通过namespace声明。

    1. namespace X {
    2. export interface Y {}
    3. export class Z {}
    4. }
    5. // ... elsewhere ...
    6. namespace X {
    7. export var Y: number;
    8. export namespace Z {
    9. export class C {}
    10. }
    11. }
    12. type X = string;

    在这个例子里,第一个代码块创建了以下名字与含义:

    • 一个值X(因为namespace声明包含一个值,Z
    • 一个命名空间X(因为namespace声明包含一个类型,Y
    • 在命名空间X里的类型Y
    • 在命名空间X里的类型Z(类的实例结构)
    • X的一个属性值Z(类的构造函数)
    • Ynumber类型),它是值X的一个属性
    • 一个命名空间Z
    • Z,它是值X的一个属性
    • X.Z命名空间下的类型C
    • X.Z的一个属性值C
    • 类型X

    一个重要的原则是export和声明会导出或导入目标的所有含义