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
。
再次明确地说,以下事情创造价值:
let
,const
和var
声明
namespace
或module
包含值的声明
- 一个
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
类型。
只要不产生冲突,这是合法的。一般的经验法则是,值总是与其他相同名称的值相冲突,除非它们被声明为namespace
s,如果类型别名声明(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型
Y
的X
命名空间
- A型
Z
在X
命名空间(的类的实例形状)
- 作为值
Z
的属性的X
值(该类的构造函数)
第二个块创建以下名称含义:
- 值
Y
(类型的number
),其是所述的属性X
值
- 一个命名空间
Z
- 作为值
Z
的属性的X
值
- A型
C
的X.Z
命名空间
- 作为值
C
的属性的X.Z
值
- 一种
X
使用export =或import
一个重要的原则是,export
和import
声明导出或导入全部含义
的目标。