14

对于初学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
  }
}

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


Unreal
87 声望10 粉丝