Deep Dive

深潜

定义文件理论:深度潜水

构建模块以提供所需的确切API形状可能会非常棘手。例如,我们可能想要一个可以被调用的模块new来产生不同的类型,在层次结构中有多种命名类型,并且在模块对象上也有一些属性。

通过阅读本指南,您将拥有编写复杂定义文件的工具,这些文件将提供友好的API表面。本指南关注模块(或UMD)库,因为这里的选项更多。

关键概念

通过理解TypeScript的一些关键概念,您可以充分理解如何进行任何形式的定义。

类型

如果您正在阅读本指南,您可能已经大致了解TypeScript中的类型。然而,更明确的是,一种类型被引入:

  • 一个类型别名声明(type sn = number | string;

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

  • 类声明(class C { }

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

  • 一种import引用某种类型的声明

这些声明表单中的每一个都创建一个新的类型名称

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

再次明确地说,以下事情创造价值:

  • letconstvar声明

  • namespacemodule包含值的声明

  • 一个enum声明

  • 一个class声明

  • 一个import引用一个值的声明

  • 一个function声明

命名空间

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

这种区别是微妙而重要的 - 在这里,A.B不一定是一种类型或价值。

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

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

这看起来可能会让人困惑,但只要我们不会过分重载,它实际上非常方便。我们来看看这种组合行为的一些有用的方面。

内置组合

例如,精明的读者会注意到,类型价值清单class都出现了这种情况。声明class C { } 创建两件事情:一个类型C,其指的是类的实例的形状,并且一个C,其指的是类的构造函数。枚举声明的行为类似。

用户组合

假设我们写了一个模块文件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类型的值- 它们是独立的。

高级组合

某些类型的声明可以在多个声明中组合使用。例如,class C { }interface C { }可共存的,都有助于性能的C类型。

只要不产生冲突,这是合法的。一般的经验法则是,值总是与其他相同名称的值相冲突,除非它们被声明为namespaces,如果类型别名声明(type s = string)被声明,那么类型将会发生冲突,并且名称空间永远不会冲突。

我们来看看如何使用。

添加 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

请注意,我们不能添加以使用接口来键入aliases(type s = string;)。

添加使用 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直到我们namespace为它写了声明之前,没有一个名称空间。含义C为命名空间不符合的价值或意义类型冲突C由类创建的。

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

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

  • A型YX命名空间

  • A型ZX命名空间(的类的实例形状)

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

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

  • Y(类型的number),其是所述的属性X

  • 一个命名空间 Z

  • 作为值Z的属性的X

  • A型CX.Z命名空间

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

  • 一种 X

使用export =或import

一个重要的原则是,exportimport声明导出或导入全部含义的目标。