对于初学 Typescript 的同学来说,声明文件一定是一个让人十分头疼、但是怎么也绕不开的问题。声明文件的书写和声明合并在 Typescript 官方文档里面已经有比较详细的介绍,这里不再陈述。我们重点讨论的是如何扩展第三方模块中的类型。
类型扩展的基本原则
如何扩展第三方模块中的类型,有三条基本原则:
- 同模块:声明合并只能在同一个模块中进行
- 同路径:声明的模块路径必须与目标类型(你将要扩展的类型)的 原始声明文件 路径保持一致
- 同书写方式:声明书写方式必须与目标类型一致
下面来详细展开
原则一:同模块
声明合并只能在同一个模块中进行。意思是说,在扩展一个类型之前,你需要先引入这个类型所在的模块。
在下面这个例子中,我们需要为接口 Foo
扩展一个属性 Bar
,Foo
是在 moduleOfFoo
中声明的,为此我们需要先引入moduleOfFoo
:
// 引入 `Foo` 所在的模块 `moduleOfFoo`,这一步非常重要
import 'moduleOfFoo'
然后我们需要声明一个同名的模块,在模块内部进行 Foo
的声明合并
// 引入 `Foo` 所在的模块 `moduleOfFoo`,这一步非常重要
import 'moduleOfFoo'
// 声明同名模块
declare module 'moduleOfFoo' {
// 在这个空间内才可以进行声明合并
interface Foo {Bar: any}
}
原则二:同路径
声明的模块路径必须与目标类型(你将要扩展的类型)的 原始声明文件 路径保持一致。
我们来看一个例子:
首先我们在 a.d.ts
中声明了interface A
// a.d.ts
export declare interface A {a: number}
b.d.ts
引用了 A
然后导出
// b.d.ts
export {A} from './a'
现在我们来扩展interface A
import './b'
declare module './a' {
interface A {test: number}
}
注意这里 declare module './a'
,因为interface A
就是在 './a'
中定义的,必须在这个模块中才能够合并声明。
顺便说一下,这里的 import './b'
,改成import './a'
也是可以的,因为都能达到引入 interface A
的目的。唯有 declare module './a'
不可以改成 declare module './b'
,因为'./b'
不是 interface A
的原始声明文件。
原则三:同书写方式
声明书写方式必须与目标类型一致。这里主要是说 namespace 嵌套关系要保持一致。
在下面这个例子中我们将要为 joint.dia.CellView
扩展两个方法 getData
和setData
。
通过观察 joint.d.ts
,我们得知CellView
嵌套了两层namespace
:
export namespace dia {
// ...
export namespace CellView {// ...}
// ...
}
所以我们在合并声明的时候,也需要嵌套两层同样的namespace
// 扩展 jointjs
import jointjs from 'jointjs'
declare module 'jointjs' {
namespace dia {
interface CellView {getData: (key?: string) => any
setData: (data: any, value?: any) => void
}
}
}
Typescript 声明合并的规则在官方文档有详细的解释,大家感兴趣可以去看看。需要注意的是:
- 声明合并无法覆盖原有的类型
- 类不能与其它类或变量合并
// a.d.ts
export declare interface A {
a: number
b: number
}
export declare let B: number
export declare class C {a: number}
我们希望将 A.a
和B
的类型改为string
,直接覆盖声明是无效的:
// custom.d.ts
import './a'
declare module './a' {
// 直接覆盖属性 a 无效
interface A {a: string}
// 直接覆盖类型 B 无效
let B: string
}
如果你实在需要覆盖 A.a
的类型,可以考虑使用继承:
// code.ts // 这里不是声明文件,是实实在在的 ts 代码
import {A as _A} from './a'
export interface A extends _A {a: string}
当然这里也并没有覆盖 A.a
的类型,不过你可以使用这个新的 code.ts
中的interface A
。
现在我们需要扩展 class C
的实例属性和静态属性,直接用 class
覆盖是无效的
// custom.d.ts
import './a'
declare module './a' {
// 直接覆盖 class 无效
class C {
b: number
static c: number
}
}
我们可以用 interface
来扩展 class
的实例属性,用 namespace
来扩展 class
的静态属性:
// custom.d.ts
import './a'
declare module './a' {
// 使用 interface 扩展 class 的实例属性
interface C {b: number}
// 使用 namespace 扩展 class 的静态属性
namespace C {let c: number}
}
如果你在扩展第三方类型的时候遇到问题,请参考以上这三条原则,相信你能够找到答案~