这篇笔记记录书写高质量的 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)
}
针对模块化的库,有三种模板:
如果模块能够作为函数被调用,使用 module-function.d.ts 模板:
import module from 'module'
let x = module(42)
如果模块能够使用 new
来构造,使用 module-class.d.ts 模板:
import Module from 'module'
let x = new Module()
如果模块不能被调用或构造,使用 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 模块,并且在非模块化环境下暴露一个全局变量 '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
}
// 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
}
}
// 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
}
一个模块插件可以改变其他模块的结构。例如 momentjs
中 moment-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.prototype
或 String.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 模块(此时你的库肯定不是全局库),直接使用 import
语句:
import * as moment from 'moment'
function getThing(): moment
从全局库
如果你的全局库依赖于一个 UMD 模块, 使用三斜线指令:
/// <reference types="moment"/>
function getThing(): moment
从模块或 UMD 库
如果你的模块或者 UMD 模块依赖于一个 UMD 模块,使用 import
语句:
注意:不要使用三斜线指令!
import * as moment from 'moment'
function getThing(): moment
不要使用如下类型: String
、Number
、Boolean
、Object
, 这些是原始类型的包装对象,应该使用 string
、number
、boolean
、object
。
/* 错误 */
interface Fetcher {
getObject(done: (data: any, elapsedTime?: number) => void): void
}
不要在回调函数的签名中使用可选参数(如这里的 elapsedTime
参数),因为 类型兼容性
允许目标函数的参数少于源函数,所以总是可以提供一个接受较少参数的函数作为回调函数。
所以应该改成:
interface Fetcher {
getObject(done: (data: any, elapsedTime: number) => void): void
}