书写声明文件:实践

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-07-12

扩展第三方模块中的类型

类型扩展的基本原则

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

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

同模块

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

// 为接口Foo扩展一个属性Bar,Foo是在moduleOfFoo中声明的
// 为此我们需要先引入moduleOfFoo 这一步非常重要
import 'moduleOfFoo';

// 声明同名模块
declare module 'moduleOfFoo' {
  // 在这个空间内才可以进行声明合并
  interface Foo {
    Bar: any
  }
}

举例,扩展axios

import { AxiosRequestConfig } from 'axios';

declare module 'axios' {
  export interface AxiosInstance {
    <T>(config: AxiosRequestConfig): Promise<T>;
  }
}

同路径

首先我们在a.d.ts中声明了interface A,b.d.ts引用了A然后导出,扩展interface A;

// a.d.ts
export declare interface A {
  a: number
}

// b.d.ts
export { A } from './a';

// index.d.ts
import './a';
// import './b' 也行, 因为在b也导入了a,也能达到引入 interface A 的目的

declare module './a' {
  interface A {
    test: number
  }
}

同书写方式

声明书写方式必须与目标类型一致。这里主要是说 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
    }
  }
}

注意点

声明合并无法覆盖原有的类型

// a.d.ts
export declare interface A {
  a: number
  b: number
}
export declare let B: number
export declare class C {
  a: number
}

// custom.d.ts
import './a'
declare module './a' {
  // 直接覆盖属性a无效
  interface A {
    a: string
  }
  // 直接覆盖类型B无效
  let B: string

  // 直接覆盖class无效
  class C {
    b: number
    static c: number
  }

  // 使用interface扩展class的实例属性
  interface C {
    b: number
  }

  // 使用namespace扩展class的静态属性
  namespace C {
    let c: number
  }
}

如果你实在需要覆盖A.a的类型,可以考虑使用继承:

// code.ts 这里不是声明文件,是实实在在的ts代码
import { A as _A } from './a'
export interface A extends _A {
  a: string
}

在项目中的声明文件

// /src/api/server-api.ts  导出的模块
// /src/api/server-api.d.ts 声明文件
declare module '@/src/api/server-api.ts' { // 其他文件引入的路径
  export default class ServerApi extends Vue {
    getData(): void;
  }
}

组合

内置组合

比如,class同时出现在类型和值列表里。class C { }声明创建了两个东西:类型C指向类的实例结构,值C指向类构造函数。枚举声明拥有相似的行为。

用户组合

export var Bar: { a: Bar };
export interface Bar {
  count: number;
}
import { Bar } from './foo'; // 可以解构引入
let x: Bar = Bar.a;
// x 指定为 Bar 类型, Bar.xx 中的 Bar 却是值
console.log(x.count);

高级组合

有一些声明能够通过多个声明组合。 比如,class C { }interface C { }可以同时存在并且都可以做为C类型的属性。

只要不产生冲突就是合法的。一个普通的规则是:

  • 值总是会和同名的其它值产生冲突除非它们在不同命名空间里
  • 类型冲突则发生在使用类型别名声明的情况下(type s = string),命名空间永远不会发生冲突。

举例:

interface Foo {
  x: number;
}
// ... elsewhere ...
interface Foo {
  y: number;
}
class Foo {
  z!: number;
}
let a: Foo = {x: 1, y: 2, z: 3};
console.log(a.x + a.y + a.z); // OK

namespace声明可以用来添加新类型,值和命名空间,只要不出现冲突。

class C {
}
// ... elsewhere ...
namespace C {
  export let x: number;
  export interface D { }
}
let y: C.D; // OK
let z: C.x; // OK

在这个例子里,直到我们写了namespace声明才有了命名空间C。做为命名空间的C不会与类创建的值C或类型C相互冲突。namespace C 是一个命名空间的同时也是一个值,因为它的声明包含了一个另一值 x。