如何快速编写第三方包的类型声明

如何快速编写第三方包的类型声明

TIPS: 本篇指南面向未接触过 TypeScript 或刚刚接触 TypeScript 的初学者,欢迎经验丰富的 TypeScript 高手进行指正。

随着微软的大力推广,越来越多的开发者开始使用 TypeScript,TypeScript 正在变得越来越流行,前端开发者们开始体验到静态类型的强大,并开始告别『动态一时爽,重构火葬场』的尴尬境地。

但同时,目前仍有大量第三方类库或公司内部历史项目仍是用纯 js 编写的,将这些代码全部重构成 TypeScript 显然是不现实的,贴心的 TypeScript 团队也为我们提供了另一种更轻便的方式让开发者快速地将第三方包集成到自己的 TypeScript 项目中,这就是今天要介绍的.d.ts类型文件。

.d.ts 中的 d 可以理解为 definition 或 declaration 即『定义或声明』,顾名思义这是对 js 文件的类型声明文件,因此我们需要关注的是对包中导出内容相关的类型,而不必关心其具体实现,简单来说,我们只需要关心包的公开 API 即可。

首先我们需要明确包使用的导出规范,global / umd / commonjs / module 等。

对于 global 导出的包我们使用:

declare namesapce MyLib {
  class A {}

  // 我们可以直接在代码中使用
  // const a = new MyLib.A()
}

对于 umd/commonjs 导出的包我们使用:

declare module 'my-lib' {
  namespace MyLib {
  class A {}

  class B {}

  // 使用时
  // 我们可以使用
  // import * as MyLib from 'my-lib'
  // const a = new MyLib.A();

  // 如果开启了 ES Module 融合模式 (esModuleInterop=true)
  // 我们可以使用
  // import { A } from 'my-lib'
  // const a = new A()
  }
  export = MyLib
}

对于 ES Module 导出的包我们使用:

declare module 'my-lib' {
  class MyLib {}

  export default MyLib

  // or other exorts
  export class A {}

  // 我们可以使用
  // import MyLib, {A} from 'my-lib'
  // const lib = new MyLib()
  // const a = new A()
}

简单了解了不同模块导出格式的书写我们开始正式根据 API 来编写内容。

我们以一个 API 非常简单的包 invert-color 为例,它只对外提供 3 个 API:

  • invert(color[, bw])
  • invert.asRGB(color[, bw])
  • invert.asRgbArray(color[, bw])

具体到 invert(color[, bw]) 中,color 类型为 String|Array|Objectbw 可选,类型为 Boolean|Object,返回值均为 String,即 :

// 错误示范
export const invert: (color: String | Array | Object, bw?: Boolean | Object) => String

首先 TypeScript 类型声明我们不应该使用 Number/String/Boolean/Object 等包装类(这些类在 js 中很少直接使用并以 API 示人),而应该直接使用基本类型声明 number/string/boolean/object,另外我们看到示范中的 Array/Object 对我们并没有特别好的提示作用,我们调用时依然无法知道该传什么类型的数组,对象的格式应该是什么样的,因此我们需要再查看一下 API 文档并重新定义一下类型:

// 字面量数组类型,明确提示应使用长度为 3 且类型均为数字的数组,对于明确长度的数组定义我们不应该使用 number[] 这样的不定长度数组进行声明
export type RgbArray = [number, number, number]

// 对于对象类型,我们使用 interface 替代 type 声明,方便可能的扩展和继承
export interface RGB {
  r: number
  g: number
  b: number
}

export interface BlackWhite {
  black: string
  white: string
  threshold?: number
}

export const invert = (color: string | RgbArray | RGB, bw?: boolean | BlackWhite) => string

可以看到我们的类型声明文件一下长了许多,但这将为我们使用 invert 方法提供更安全、更优雅的体验。但我们只定义了 invert 方法,接下来考虑一下 invert.asRGBinvert.asRgbArray,这两个方法均挂载在 invert 函数上,一般的我们会用 namespace 来添加属性方法,但目前 TypeScript 开始推荐使用 ES Module 融合模式而不是 namespace 来解决,因此这里我们来使用另一种方式扩展:

export type Color = string | RgbArray | RGB

export interface InvertColor {
  (color: Color, bw?: boolean | BlackWhite): string // interface 可以直接定义函数体
  asRGB(color: Color, bw?: boolean | BlackWhite): RGB
  asRgbArray(color: Color, bw?: boolean | BlackWhite): RgbArray
}

export const invert: InvertColor;

至此,一个完整的 .d.ts 类型声明就完成了!

接下来,我们开始介绍一下其他技巧:

// 函数重载,我们应只对返回值不同的函数进行声明重载,否则可以将参数进行合并
function fn(): number
function fn(input: string): string

// 泛型,可以使用时动态设置返回值类型
export function request<T>(): Promise<T>
// 如 const result = request<{a: string}>(); result.a

// 动态键值对类型,配合泛型可以定义一个通用接口
export interface KeyValue<T = any> {
  locked: boolean // 也可以有部分固定的属性
  [key: string]: T
}
// TypeScript 提供一个通用的全局 Record 类型
type R = Record<string, string>

// keyof 获取对象所有 key 值字面量
interface Value {
  key1: string
  key2: boolean
}

type Keys = keyof Value // 即 type Keys = 'key1' | 'key2'

// in 遍历字面量类型
type Value2 = {
  [key in Keys]: Value[key]
}
// 即 type Values = { key1: string; key2: boolean }

// typeof 获取字面量的类型
class A {}

// 这里表示参数是 class A 这个类,如果没有 `typeof` 则参数是 class A 的实例
export const callClassA(classA: typeof A)

// Exclude 排除指定类型
type Key1 = Exclude<Keys, 'key2'> // 即 type Key1 = 'key1'

// 以下扩展全局属性的方式应尽量避免使用

// 扩展新增全局变量
declare global {
const myGlobalVar: string // 将能在全局直接使用 myGlobalVar
}

// 扩展新增 Node global 属性
declare namespace NodeJS {
  interface Global {
    myGlobalVar: string // 将能在全局使用 global.myGlobalVar
  }
}

// 扩展新增 window 属性
declare interface Window {
  myGlobalVar: string // 将能在全局使用 window.myGlobalVar
}

// 针对文件后缀声明类型,如使用 CSS Modules
declare module '*.less' {
  const styles: Record<string, string>;
  export = styles;
}

虽然有这么多类型声明的技巧,但其实最后的难点在于如何把不同的部分组合使用起来,我们以一个相对复杂的实例 hoist-non-react-statics 作为收尾:

import * as React from 'react';

interface REACT_STATICS {
  childContextTypes: true;
  contextTypes: true;
  defaultProps: true;
  displayName: true;
  getDefaultProps: true;
  getDerivedStateFromProps: true;
  mixins: true;
  propTypes: true;
  type: true;
}

interface KNOWN_STATICS {
  name: true;
  length: true;
  prototype: true;
  caller: true;
  callee: true;
  arguments: true;
  arity: true;
}

declare function hoistNonReactStatics<
  T extends React.ComponentType<any>,
  S extends React.ComponentType<any>,
  C extends {
    [key: string]: true
  } = {}
>(
  TargetComponent: T,
  SourceComponent: S,
  customStatic?: C,
): T &
{
  [key in Exclude<
    keyof S,
    // only extends static properties, exclude instance properties and known react statics
    keyof REACT_STATICS | keyof KNOWN_STATICS | keyof C
  >]: S[key]
};

export = hoistNonReactStatics;

大家可以自行理解一下上面复制但排除指定静态属性的定义部分,如果能够理解那么相信本篇指南对你有效,如果没有欢迎评论探讨疑问。

大家也可以到 DefinitelyTyped 查看更多优质类型定义文件,并贡献自己的力量。

希望大家工作之余有额外的精力时可以帮助完善公司内部基础库的类型定义声明,让一线开发者们即使没有文档也能写出稳健的代码,当然更美好的目标是:现在就开始使用 TypeScript 来编写源代码。

本文由来源 快速编写第三方包 .d.ts 类型声明指南 - 柳家忍,由 开发指南 整理编辑,其版权均为 快速编写第三方包 .d.ts 类型声明指南 - 柳家忍 所有,文章内容系作者个人观点,不代表 开发指南 对观点赞同或支持。如需转载,请注明文章来源。
5
开发指南

发表评论