本文使用的 TypeScript 版本为 5.3.2。示例代码已上传到 GitHub (opens new window)

在 TypeScript 项目中难免会用到一些 JS 库,TypeScript 官方支持使用 @types/ 的同名库来作为 JS 库的类型定义。如果 @types/ 里对应的库不存在,或者类型不全,开发者仍然需要自行补充类型。

# 向 DefinitelyTyped 提 PR

如果 @types/ 里对应的库不存在,或者类型不全,可以直接向 DefinitelyTyped/DefinitelyTyped (opens new window) 提 PR,创建或者更新对应的库。

DefinitelyTyped 虽然是一个非常庞大的 monorepo,但是有很完善的贡献代码和 review 的流程:首先会 request 包作者的 review,如果包作者没有 review,会邀请项目的维护者来 review。提完 PR 以后大概一个月之内就能合并,合并之后等待发版,安装新的 @types/ 就能用上我们贡献的类型了,不需要在我们的项目中做更多的配置。


当然,并不是所有场景都适合向 DefinitelyTyped 提 PR。比如:

  1. 项目需要立即使用这些类型,等待 PR 合并的流程太过漫长
  2. 项目只用到了 JS 库的一小部分代码,为 JS 库创建一个完整的 @types 库会耗费过多精力

在这些情况下,我们仍然可以在自己的项目中为 JS 库中补充类型。不过大部分博客都没有讲到如何在本地库添加类型、补充类型,这也是本文的目的所在。

# 为 JS 库扩展类型

如果你使用的库具有 @types 库,但是库不全,你可以为其补充类型。

有下面一段 JS 代码,可以正常运行,但缺少 BMapGL.DistrictLayer 类和 BMapGL.Map.addDistrictLayer() BMapGL.Map.removeDistrictLayer() 方法的类型。

// https://github.com/lyh543-lab/typescript-custom-typing-example/blob/main/src/App.tsx#L10
function CustomDistrictLayer() {
  const {map} = useMapContext();  // map 的类型是 BMapGL.Map
  useEffect(() => {
    if (!map) {
      return;
    }
    const districtLayer = new BMapGL.DistrictLayer({
      name: ['北京市'],
    });
    map.addDistrictLayer(districtLayer);
    return () => map.removeDistrictLayer(districtLayer);
  }, [map]);

  return null;
}

我们可以直接新建 src/global.d.ts(或者别的 src/**/*.d.ts),写入以下内容:

/// <reference types="react-bmapgl/types/bmapgl" />

declare namespace BMapGL {
  /**
   * @link https://lbsyun.baidu.com/index.php?title=district
   */
  export class DistrictLayer {
    constructor(options: {
      name: string[];
      kind?: number;
      strokeColor?: string;
      strokeOpacity?: number;
      fillColor?: string;
    });
  }

  export interface Map {
    addDistrictLayer(layer: DistrictLayer): void;
    removeDistrictLayer(layer: DistrictLayer): void;
  }
}

/// <reference types="react-bmapgl/types/bmapgl" /> 可以理解成 .d.ts 之间的 import,不过 import 更倾向于执行包里的声明语句然后引入一部分命名,<reference /> 更倾向于引入一个库并追加更多的类型。

这样可以在 BMapGL 下新定义 DistrictLayer 类,然后为 Map 类拓展两个新的方法。新建完跑一下 pnpm run tsc 就可以确认没问题了。

# 为 JS 库添加类型

如果你使用的库是纯 JS 库,也没有一点 TS 定义,你可以选择创建一个新的目录,作为这个库的类型定义。

下面的例子中,mapvgl 就是一个纯 JS 库。

// https://github.com/lyh543-lab/typescript-custom-typing-example/blob/main/src/App.tsx#L26
import * as mapvgl from 'mapvgl';

function CustomMapVGLLayer() {
  const {map} = useMapContext();
  useEffect(() => {
    if (!map) {
      return;
    }
    const view = new mapvgl.View({map});
    const layer = new mapvgl.PointLayer({
      color: '#E91E63',
      shape: 'circle', // 默认为圆形,可传square改为正方形
      blend: 'lighter',
      size: 20,
      data: [{
        geometry: {
          type: 'Point',
          coordinates: [116.402544, 39.928216]
        }
      }]
  });
  view.addLayer(layer);
  return () => view.destroy();
  }, [map]);

  return null;
}

为此,可以创建一个 src/@types/mapvgl/index.d.ts(或者别的 src/**/mapvgl/index.d.ts),定义自己项目里需要的那些 API:

// https://github.com/lyh543-lab/typescript-custom-typing-example/blob/main/src/%40types/mapvgl/index.d.ts

/// <reference types="react-bmapgl/types/bmapgl" />

// docs: https://mapv.baidu.com/gl/docs/index.html
// You can send docs into ChatGPT to generate typescript definitions

/* eslint-disable @typescript-eslint/no-explicit-any */

export interface ViewOptions {
  map: BMapGL.Map;
  mapType?: 'bmap' | 'blank' | 'cesium';
  effects?: Array<any>; // The specific effect type can replace 'any' if available
}

/**
 * @link https://mapv.baidu.com/gl/docs/View.html
 */
export class View {
  constructor(options: ViewOptions);
  addLayer(layer: Layer): void;
  removeLayer(layer: Layer): void;
  removeAllLayers(): void;
  getAllLayers(): Layer[];
  getAllThreeLayers(): ThreeLayer[];
  hide(): void;
  show(): void;
  hideLayer(layer: Layer): void;
  showLayer(layer: Layer): void;
  destroy(): void;
}

// ...

定义完以后跑 pnpm run tsc,会发现仍然会报错,tsc 提示不存在 @types/mapvgl 这个库。我们虽然定义了 ViewOptions 接口和 View 类,但 tsc 并不知道我们定义的是 mapvgl 库里的 ViewOptionsView。所以需要在 tsconfig.json 里关联一下:

// https://github.com/lyh543-lab/typescript-custom-typing-example/blob/main/tsconfig.json
{
  "compilerOptions": {
    // works on TypeScript 4.7+
    "paths": {
      "mapvgl": ["./src/@types/mapvgl/index.d.ts"],
    },
    // works on TypeScript 4.6
    // on TypeScript 4.7+, typeRoots seems not working without defining paths
    "typeRoots": [ "./src/types", "./node_modules/@types"],
  }
}

再跑 tsc 就没问题啦。