乐趣区

Typescript声明文件第三方类型扩展

对于初学 Typescript 的同学来说,声明文件一定是一个让人十分头疼、但是怎么也绕不开的问题。声明文件的书写和声明合并在 Typescript 官方文档里面已经有比较详细的介绍,这里不再陈述。我们重点讨论的是如何扩展第三方模块中的类型。

类型扩展的基本原则

如何扩展第三方模块中的类型,有三条基本原则:

  1. 同模块:声明合并只能在同一个模块中进行
  2. 同路径:声明的模块路径必须与目标类型(你将要扩展的类型)的 原始声明文件 路径保持一致
  3. 同书写方式:声明书写方式必须与目标类型一致

下面来详细展开

原则一:同模块

声明合并只能在同一个模块中进行。意思是说,在扩展一个类型之前,你需要先引入这个类型所在的模块。

在下面这个例子中,我们需要为接口 Foo 扩展一个属性 BarFoo 是在 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 扩展两个方法 getDatasetData
通过观察 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 声明合并的规则在官方文档有详细的解释,大家感兴趣可以去看看。需要注意的是:

  1. 声明合并无法覆盖原有的类型
  2. 类不能与其它类或变量合并
// a.d.ts
export declare interface A {
  a: number
  b: number
}
export declare let B: number
export declare class C {a: number}

我们希望将 A.aB的类型改为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}
}

如果你在扩展第三方类型的时候遇到问题,请参考以上这三条原则,相信你能够找到答案~

退出移动版