Appearance
1️⃣ 声明文件理论:深入
构造模块以提供你想要的确切API形状可能很棘手。比如,我们可能希望一个模块能够使用 new
或者不使用 new
产生不同类型,在层次结构中暴露了各种命名类型,并在模块对象上存在一些属性。 通过阅读这个指南,你将使用工具写出更为复杂的声明文件,友好的暴露API表面。本指南关注模块(或UMD),因为它们的配置变化更加丰富。
2️⃣ 关键概念
只有理解了TypeScript是如何运作的核心概念后,你才能完全理解如何制作任意想要的声明。
2.1 类型(Types
)
如果你正在阅读本指南,你可能大概已经知道TS类型是什么了。但是,更明确地说,下面方式会产生类型
:
- 一个类型别名声明(
type sn = number | string
) - 一个接口声明(
interface I { x: number[] }
) - ✨ 一个类声明(
class C {}
) - 一个枚举声明(
enum E {A, B, C}
) - 一个
import
声明,用于引用某个类型
上面的每一种声明形式都会创建一个新的类型名😎。
2.2 值(Values
)
通过类型,你可能已经理解了什么是值。值是我们可以在表达式中引用的运行时名。比如 let x = 5;
创建一个称之为 x
的值。
同样,为了更加明显,下面的东西会创建值:
let, const & var
声明namespace | module
声明会包含值enum
声明- ✨
class
声明 import
声明会引用一个值function
声明
2.3 命名空间(Namespaces
)
类型可存在于 命名空间
中。比如,如果我们有 let x: A.B.C
这样的声明,表示类型 C
来自于 A.B
命名空间。
这个区别是微妙但重要的 - 这里, A.B
不必是一个类型或者值。
3️⃣ 简单组合:一个名字,多重意思
给定一个名字 A
,我们可能找到 A
的3个不同含义:
- 一种类型
- 一个值
- 一个命名空间
该名字如何被理解,完全取决于它被使用的上下文环境。
比如,在 let m: A.A = A;
声明中,A
首先被用作命名空间,然后被当做一种类型名,最后当做是一个值。这些含义最终会指向完全不同的声明。
这看起来可能很困惑,但实际上它非常方便,只要我们不过度使用这些东西。让我们看看这种结合行为的有用之处。
3.1 内置组合
😎机敏的读者会注意到这一点,比如,class
同时出现在 type
和 value
列表中。class C {}
声明创建了2个东西:
- 一个
类型C
,它指向该类的实例, - 以及一个
值C
,它指向该类的构造函数。
TIP
枚举声明表现类似。
3.2 ⭐ 用户组合
假设写一个模块文件 foo.d.ts
:
typescript
export var SomeVar: { a: SomeType };
export interface SomeType {
count: number;
}
然后使用它:
typescript
import * as foo from './foo'
let x: foo.SomeType = foo.SomeVar.a
console.log(x.count)
这很好用,但我们可能会想象 SomeType
和 SomeVar
非常密切相关,因此你希望它们具有相同的名称。我们可以使用组合表示这2个不同对象(值和类型)在相同的名字 Bar 中
:
typescript
export var Bar: { a: Bar }
export interface Bar {
count: number;
}
这为在消费代码中解构提供了一个非常好的机会:
typescript
import { Bar } from './foo'
let x: Bar = Bar.a
console.log(x.count)
再一次,这里我们使用 Bar
同时作为类型和值在使用😎。注意,我们不需要声明 Bar
值作为 Bar
类型 - 它们是独立的。
4️⃣ 高级组合
📚某些声明可以跨多个声明进行组合。比如,class C {}
和 interface C {}
能共存,并且同时给 C
类型贡献属性。
这是合法的,只要它们不产生冲突。一般的经验法则是,值总是与同名的其他值冲突,除非它们被声明为 namespace s
,类型会冲突,如果它们被声明为类型别名(type s = string
), 命名空间永远不会冲突。
我们看看如何使用它们。
4.1 🚀 使用 interface
添加
我们可以使用 interface
给另一个 interface
声明添加额外的成员:
typescript
interface Foo {
x: number;
}
// 别的地方
interface Foo {
y: number;
}
let a: Foo = ...;
console.log(a.x + a.y) // OK
这同样适用于类🤩:
typescript
class Foo {
x: number;
}
// 别的地方
interface Foo {
y: number
}
let a: Foo = ...;
console.log(a.x + a.y) // OK
WARNING
我们不能使用interface添加类型别名(type s = string
)
4.2 使用 namespace
添加
命名空间(namespace
)声明可用于添加新的类型、值和命名空间,但不会产生冲突。
例如,我们可以给一个类添加静态成员:
typescript
class C {}
// 某个地方
namespace C {
export let x: number;
}
let y = C.x; // OK
注意这个例子,我们向C
的静态端(其构造函数)添加了一个值。这是因为我们添加了一个 value
,所有值的容器是另一个值(类型由命名空间包含,命名空间由其他命名空间包含)。
我们也可以给类添加一个命名空间类型:
typescript
class C {}
// 某个地方
namespace C {
export interface D {}
}
let y: C.D // OK
在本例中,直到我们为命名空间C
编写了命名空间声明,才有命名空间C
。C
作为命名空间的含义不会和 class C
作为值或类型的含义相冲突。
最后,我们可以使用 namespace
声明执行很多不同的合并。这不是一个特别现实的例子,但显示了各种有趣的行为:
typescript
namespace X { // X 作为命名空间
export interface Y {} // Y 作为类型
export class Z {} // Z 既是值也是类型
}
// 别处某个地方
namespace X { // X 作为命名空间
export var Y: number; // Y 作为值
export namespace Z { // Z 作为命名空间
export class C {}
}
}
type X = string; // X作为类型
上面例子中,第一块创建了下面name含义:
- 一个值
X
(因为namespace
声明包含一个值Z
) - 一个命名空间
X
(因为namespace
声明包含一个类型Y
) - 一个在
X
命名空间中的一个类型Y
- 一个在
X
命名空间中的一个类型Z
(类的实例) - 一个值
Z
,它是X
值的一个属性(该类的构造函数)
第2块创建下面name含义:
- 一个
Y
值(类型number
),它是X
值得一个属性 - 一个命名空间
Z
- 一个值
Z
,它是X
值得一个属性 - 一个在
X.Z
命名空间中类型C
- 一个值
C
它是X.Z
值的属性 - 一个类型
X
原文档:
2022年09月04日13:05:13