learn-typescript

声明文件

介绍

这篇笔记记录书写高质量的 TypeScript 声明文件的注意点。

结构

全局库

全局库的声明文件模板 global.d.ts

// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>

/*~ 如果该库可以被直接调用,例如:`myLib(3)`,
 *~ 参考如下调用签名写法,
 *~ 否则删除这部分。
 */
declare function myLib(a: string): string
declare function myLib(a: number): number

/*~ 如果想要把库名作为一个类型名,例如:` var x: myLib `,需要如下声明,
 *~ 否则删除这部分。
 */
interface myLib {
  name: string
  length: number
  extras?: string[]
}

/*~ 如果库暴露的全局变量名有属性,写在这里,
 *~ 类型定义(如: interface, type alias)等也写在这里。
 */
declare namespace myLib {
  //~ 可以这么写: `myLib.timeout = 50`
  let timeout: number

  //~ 可以获取只读变量 `myLib.version`
  const version: string

  //~ 定义类,可以这么使用:` let c = new myLib.Cat(42) `
  //~ 或者作为类型: ` function f(c: myLib.Cat) { ... } `
  class Cat {
    readonly age: number
    constructor(n: number)
    purr(): void
  }

  //~ 声明接口,可以这么调用:
  //~ ` var s: myLib.CatSettings = { weight: 5, name: "Maru" }; `
  interface CatSettings {
    weight: number
    name: string
    tailLength?: number
  }

  //~ 定义类型别名,使用方法: `const v: myLib.VetID = 42;`
  //~ 或者 `const v: myLib.VetID = "bob";`
  type VetID = string | number

  //~ 定义方法,可以这么调用: ` myLib.checkCat(c)` 或者 `myLib.checkCat(c, v);`
  function checkCat(c: Cat, s?: VetID)
}

模块化的库

针对模块化的库,有三种模板:

  1. 如果模块能够作为函数被调用,使用 module-function.d.ts 模板:

    import module from 'module'
    let x = module(42)
    
  2. 如果模块能够使用 new 来构造,使用 module-class.d.ts 模板:

    import Module from 'module'
    let x = new Module()
    
  3. 如果模块不能被调用或构造,使用 module.d.ts 模板。

module-function.d.ts

// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>

/*~ 如果该模块是 UMD 模块,并且在非模块化环境下暴露一个全局变量 'myFuncLib' ,
 *~ 那么像下面这样声明,否则删除这部分
 */
export as namespace myFuncLib

/*~ 这个声明指示 `MyFunction` 函数为该文件的导出对象 */
export = MyFunction

/*~ 函数重载声明的写法 */
declare function MyFunction(name: string): MyFunction.NamedReturnType
declare function MyFunction(length: number): MyFunction.LengthReturnType

/*~ 模块中的一些类型信息(例如函数返回值类型等)可以放在这个部分,
 *~ 如果该模块还有属性,也在这里声明
 */
declare namespace MyFunction {
  export interface LengthReturnType {
    width: number
    height: number
  }
  export interface NamedReturnType {
    firstName: string
    lastName: string
  }

  /*~ 获取属性的写法如下:
   *~ import f = require('myFuncLibrary')
   *~ console.log(f.defaultName)
   */
  export const defaultName: string
  export let defaultLength: number
}

module-class.d.ts

// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>

/*~ 如果该模块是 UMD 模块,并且在非模块化环境下暴露一个全局变量 'myClassLib' ,
 *~ 那么像下面这样声明,否则删除这部分
 */
export as namespace myClassLib

/*~ 这个声明指示 `MyClass` 构造函数为该文件的导出对象 */
export = MyClass

/*~ 在这里声明类的属性和方法 */
declare class MyClass {
  constructor(someParam?: string)

  someProperty: string[]

  myMethod(opts: MyClass.MyClassMethodOptions): number
}

/*~ 这里写想要导出的一些类型 */
declare namespace MyClass {
  export interface MyClassMethodOptions {
    width?: number
    height?: number
  }
}

module.d.ts

// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>

/*~ 如果该模块是 UMD 模块,并且在非模块化环境下暴露一个全局变量 'myLib' ,
 *~ 那么像下面这样声明,否则删除这部分
 */
export as namespace myLib

/*~ 方法的声明 */
export function myMethod(a: string): string
export function myOtherMethod(a: number): number

/*~ 接口声明 */
export interface someType {
  name: string
  length: number
  extras?: string[]
}

/*~ 可以通过 const, let, var 声明属性 */
export const myField: number

/*~ 如果导出一个对象 'subProp', 声明方法如下 */
export namespace subProp {
  /*~ 可以通过如下写法获取属性
   *~   import { subProp } from 'yourModule';
   *~   subProp.foo();
   *~ 或者
   *~   import * as yourMod from 'yourModule';
   *~   yourMod.subProp.foo();
   */
  export function foo(): void
}

模块插件库

一个模块插件可以改变其他模块的结构。例如 momentjsmoment-range 添加了新的 range 方法到 moment 对象上。模块插件库的声明文件使用 module-plugin.d.ts 模板。

// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>

/*~ 导入想要修改的模块 */
import * as m from 'someModule'

/*~ 如果需要,还可以导入其他的模块 */
import * as other from 'anotherModule'

/*~ 这里声明和上面导入的想要修改的模块一样的模块 */
declare module 'someModule' {
  /*~ 可以添加新的方法,类或者变量。
   *~ 如果需要,可以使用原模块中未导出的类型
   */
  export function theNewMethod(x: m.foo): other.bar

  /*~ 可以为已存在的 interface 添加属性,因为同名接口可以进行声明合并 */
  export interface SomeModuleOptions {
    someModuleSetting?: string
  }

  /*~ 还可以声明新的类型 */
  export interface MyModulePluginOptions {
    size: number
  }
}

全局插件库

一个全局插件是全局代码,会改变全局对象的结构。全局插件之间可能会存在冲突,比如一些库往 Array.prototypeString.prototype 里添加新的方法,例如下面的代码:

var x = 'hello world'
// 全局插件库向内置对象 String 原型上添加了新方法 startsWithHello
console.log(x.startsWithHello())

全局插件库的声明文件参考模板 global-plugin.d.ts

// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>

/*~ 给原来的类型添加一个声明,然后在声明中添加新的成员。
 *~ 例如,下面的代码为内置的 Number 类型添加了 'toBinaryString' 方法的签名
 */
interface Number {
  toBinaryString(opts?: MyLibrary.BinaryFormatOptions): string
  toBinaryString(
    callback: MyLibrary.BinaryFormatCallback,
    opts?: MyLibrary.BinaryFormatOptions
  ): string
}

/*~ 如果想要声明一些类型(如新增方法的参数类型、返回值类型等),把它们放到
 *~ 一个命名空间中,以避免添加太多的东西到全局命名空间中
 */
declare namespace MyLibrary {
  type BinaryFormatCallback = (n: number) => string
  interface BinaryFormatOptions {
    prefix?: string
    padding: number
  }
}

全局修改的模块

一个全局修改的模块被导入时,会改变全局对象的结构。比如存在一些模块,当被导入时会添加新的成员到 String.prototype ,这种模式也可能造成冲突。全局修改的模块类似如下效果:

// 通常只引入模块,而不在意模块的返回值
import 'global-modifying-module'

var x = 'hello world'
// 往内置对象上添加了新的方法
console.log(x.startsWithHello())

全局修改的模块的声明文件参考模板 global-modifying-module.d.ts

// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>

/*~ Note: If your global-modifying module is callable or constructable, you'll
 *~ need to combine the patterns here with those in the module-class or module-function
 *~ template files
 */

/*~ 注意: 如果该全局修改模块是可以作为函数调用的,或者是可以通过 `new` 来构造的,你将
 *~ 需要把下面这些写法和 `module-class.d.ts`、`module-function.d.ts` 中的写法组合起来
 */
declare global {
  /*~ 这里可以声明全局命名空间中的内容,新增声明或者修改已存在的声明 */
  interface String {
    fancyFormat(opts: StringFormatOptions): string
  }
}

/*~ 如果模块需要导出类型或者值,和普通模块中的声明写法一样 */
export interface StringFormatOptions {
  fancinessLevel: number
}

/*~ 例如,从模块中导出一个方法(在该模块具有的全局副作用之外) */
export function doSomething(): void

/*~ 如果模块不需要导出任何东西,就添加上该行,否则删除该行 */
export {}

使用依赖

依赖于全局库

如果你的库依赖于全局库,使用三斜线指令 /// <reference types="..."/>/// <reference path="..."/> (types 与 path 区别在于 types 引入的是 @types 包中的声明文件):

/// <reference types="someLib"/>

function getThing(): someLib.thing

依赖于模块(非 UMD 模块)

如果你的库依赖一个非 UMD 模块(此时你的库肯定不是全局库),直接使用 import 语句:

import * as moment from 'moment'

function getThing(): moment

依赖于 UMD 模块

  1. 从全局库
    如果你的全局库依赖于一个 UMD 模块, 使用三斜线指令:

    /// <reference types="moment"/>
    
    function getThing(): moment
    
  2. 从模块或 UMD 库
    如果你的模块或者 UMD 模块依赖于一个 UMD 模块,使用 import 语句:

    注意:不要使用三斜线指令!

    import * as moment from 'moment'
    
    function getThing(): moment
    

规范

使用普通类型而非包装类型

不要使用如下类型: StringNumberBooleanObject, 这些是原始类型的包装对象,应该使用 stringnumberbooleanobject

不要在回调函数的签名中使用可选参数

/* 错误 */
interface Fetcher {
  getObject(done: (data: any, elapsedTime?: number) => void): void
}

不要在回调函数的签名中使用可选参数(如这里的 elapsedTime 参数),因为 类型兼容性 允许目标函数的参数少于源函数,所以总是可以提供一个接受较少参数的函数作为回调函数。

所以应该改成:

interface Fetcher {
  getObject(done: (data: any, elapsedTime: number) => void): void
}