【译】TypeScript 类型声明文件(.d.ts)编写之深入(Definition File Theory: A Deep Dive)

通过结构化模块,为所需的 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:

  • letconstvar 声明
  • namespacemodule 声明包含一个 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 同时出现在 typevalue 清单中。 声明 class C {} 创建了两项内容:

  1. 类型 C——类 C 的实例原型
  2. 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);

这工作得很好,但我们可以想象 SomeTypeSomeVar 密切相关,所以你希望它们有相同的名字。 我们可以使用组合来以相同的名称显示这两个不同的对象(值和类型)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

在这个例子中,直到我们写了命名空间声明之前,没有一个命名空间 CC 作为命名空间的含义,与该类创建的 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 值的属性(类的构造器)

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

  • Ynumber 类型,X 值的属性)
  • 命名空间 Z
  • ZX 值的属性)
  • 类型 C(在命名空间 X.Z 中)
  • CX.Z 值的属性)
  • 类型 X

四、使用 export =import

一个重要的规则,是 exportimport 声明输出或输入它们目标的所有含义

五、参考资料

译自 Definition File Theory: A Deep Dive

(完)

你可能感兴趣的:(【译】TypeScript 类型声明文件(.d.ts)编写之深入(Definition File Theory: A Deep Dive))