global-modifying-module.d.ts

修改了全局作用域的模块

对于修改了全局作用域的模块来讲,在导入它们时,会对全局作用域中的值进行修改。
比如存在某个代码库,当导入它时,它会向String.prototype上添加新的成员。
该模式存在危险,因为它有导致运行时冲突的可能性,
但我们仍然可以为其编写声明文件。

识别出修改了全局作用域的模块

我们可以通过文档来识别修改了全局作用域的模块。
通常来讲,它们与全局插件类似,但是需要require语句来激活。

你可能看到过如下的文档:

// 'require' call that doesn't use its return value
var unused = require('magic-string-time');
/* or */
require('magic-string-time');

var x = 'hello, world';
// Creates new methods on built-in types
console.log(x.startsWithHello());

var y = [1, 2, 3];
// Creates new methods on built-in types
console.log(y.reverseAndSort());

以下是一个示例:

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

/*~ This is the global-modifying module template file. You should rename it to index.d.ts
 *~ and place it in a folder with the same name as the module.
 *~ For example, if you were writing a file for "super-greeter", this
 *~ file should be 'super-greeter/index.d.ts'
 */

/*~ 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
 */
declare global {
    /*~ Here, declare things that go in the global namespace, or augment
     *~ existing declarations in the global namespace
     */
    interface String {
        fancyFormat(opts: StringFormatOptions): string;
    }
}

/*~ If your module exports types or values, write them as usual */
export interface StringFormatOptions {
    fancinessLevel: number;
}

/*~ For example, declaring a method on the module (in addition to its global side effects) */
export function doSomething(): void;

/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
export {};

global-plugin.d.ts

UMD

一个 UMD 模块既可以用作 ES 模块(使用导入语句),也可以用作全局变量(在缺少模块加载器的环境中使用)。
许多流行的代码库,如Moment.js,都是使用这模式发布的。
例如,在 Node.js 中或使用了 RequireJS 时,你可以这样使用:

import moment = require('moment');
console.log(moment.format());

在纯浏览器环境中,你可以这样使用:

console.log(moment.format());

识别 UMD 代码库

UMD 模块会检查运行环境中是否存在模块加载器。
这是一种常见模式,示例如下:

(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define(["libName"], factory);
    } else if (typeof module === "object" && module.exports) {
        module.exports = factory(require("libName"));
    } else {
        root.returnExports = factory(root.libName);
    }
}(this, function (b) {

如果你看到代码库中存在类如typeof definetypeof windowtypeof module的检测代码,尤其是在文件的顶端,那么它大概率是 UMD 代码库。

在 UMD 模块的文档中经常会提供在 Node.js 中结合require使用的示例,以及在浏览器中结合<script>标签使用的示例。

UMD 代码库的示例

大多数流行的代码库均提供了 UMD 格式的包。
例如,jQueryMoment.jslodash等。

模版

针对模块,共存在三个模版。它们是:

若一个模块可以当作函数调用,则使用module-function.d.ts

var x = require('foo');
// Note: calling 'x' as a function
var y = x(42);

请务必阅读脚注:”ES6 对模块调用签名的影响”

如果一个模块可以使用new来构造,则使用module-class.d.ts

var x = require('bar');
// Note: using 'new' operator on the imported variable
var y = new x('hello');

请务必阅读脚注:”ES6 对模块调用签名的影响”,它同样适用于这类模块。

如果一个模块既不可以调用,又不可以构造,那么就使用module.d.ts

模块插件或 UMD 插件

模块插件会改变其它模块的结构(包含 UMD 或 ES 模块)。
例如,在 Moment.js 中,moment-range会将range方法添加到moment对象上。

对于编写声明文件而言,无论是 ES 模块还是 UMD 模块,你都可以使用相同的代码。

模版

使用module-plugin.d.ts模版。

全局插件

全局插件是一段全局代码,它会改变某个全局变量。
对于修改了全局作用域的模块,它会增加出现运行时冲突的可能性。

例如,有些库会向Array.prototypeString.prototype中增加新的函数。

识别全局插件

全局插件通常可以根据其文档来识别。

你会看到如下示例:

var x = 'hello, world';
// Creates new methods on built-in types
console.log(x.startsWithHello());

var y = [1, 2, 3];
// Creates new methods on built-in types
console.log(y.reverseAndSort());

模版

使用global-plugin.d.ts模版。

修改了全局作用域的模块

对于修改了全局作用域的模块来讲,在导入它们时,会对全局作用域中的值进行修改。
比如存在某个代码库,当导入它时,它会向String.prototype上添加新的成员。
该模式存在危险,因为它有导致运行时冲突的可能性,
但我们仍然可以为其编写声明文件。

识别出修改了全局作用域的模块

我们可以通过文档来识别修改了全局作用域的模块。
通常来讲,它们与全局插件类似,但是需要require语句来激活对全局作用域的修改。

你可能看到过如下的文档:

// 'require' call that doesn't use its return value
var unused = require('magic-string-time');
/* or */
require('magic-string-time');

var x = 'hello, world';
// Creates new methods on built-in types
console.log(x.startsWithHello());

var y = [1, 2, 3];
// Creates new methods on built-in types
console.log(y.reverseAndSort());

模版

使用global-modifying-module.d.ts模版。

利用依赖

你的代码库可能会有若干种依赖。
本节会介绍如何在声明文件中导入它们。

对全局库的依赖

如果你的代码库依赖于某个全局代码库,则使用/// <reference types="..." />指令:

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

function getThing(): someLib.thing;

对模块的依赖

如果你的代码库依赖于某个模块,则使用import语句:

import * as moment from 'moment';

function getThing(): moment;

对 UMD 模块的依赖

全局代码库

如果你的全局代码库依赖于某个 UMD 模块,则使用/// <reference types指令:

/// <reference types="moment" />

function getThing(): moment;
ES 模块或 UMD 模块代码库

如果你的模块或 UMD 代码库依赖于某个 UMD 代码库,则使用import语句:

import * as someLib from 'someLib';

不要使用/// <reference指令来声明对 UMD 代码库的依赖。

脚注

防止命名冲突

注意,虽说可以在全局作用域内定义许多类型。
但我们强烈建议不要这样做,因为当一个工程中存在多个声明文件时,它可能会导致难以解决的命名冲突。

可以遵循的一个简单规则是使用代码库提供的某个全局变量来声明拥有命名空间的类型。
例如,如果代码库提供了全局变量cats,那么可以这样写:

declare namespace cats {
    interface KittySettings {}
}

而不是:

// at top-level
interface CatsKittySettings {}

这样做会保证代码库可以被转换成 UMD 模块,且不会影响声明文件的使用者。

ES6 对模块插件的影响

一些插件会对已有模块的顶层导出进行添加或修改。
这在 CommonJS 以及其它模块加载器里是合法的,但 ES6 模块是不可改变的,因此该模式是不可行的。
因为,TypeScript 是模块加载器无关的,所以在编译时不会对该行为加以限制,但是开发者若想要转换到 ES6 模块加载器则需要注意这一点。

ES6 对模块调用签名的影响

许多代码库,如 Express,将自身导出为可调用的函数。
例如,Express 的典型用法如下:

import exp = require('express');
var app = exp();

在 ES6 模块加载器中,顶层对象(此例中就exp)只能拥有属性;
顶层的模块对象永远不能够被调用。
最常见的解决方案是为可调用的/可构造的对象定义一个default导出;
有些模块加载器会自动检测这种情况并且将顶层对象替换为default导出。

代码库文件结构

声明文件的结构应该反映代码库源码的结构。

一个代码库可以包含多个模块,比如:

myLib
  +---- index.js
  +---- foo.js
  +---- bar
         +---- index.js
         +---- baz.js

它们可以通过如下方式导入:

var a = require('myLib');
var b = require('myLib/foo');
var c = require('myLib/bar');
var d = require('myLib/bar/baz');

声明文件如下:

@types/myLib
  +---- index.d.ts
  +---- foo.d.ts
  +---- bar
         +---- index.d.ts
         +---- baz.d.ts
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>

/*~ This template shows how to write a global plugin. */

/*~ Write a declaration for the original type and add new members.
 *~ For example, this adds a 'toBinaryString' method with overloads to
 *~ the built-in number type.
 */
interface Number {
    toBinaryString(opts?: MyLibrary.BinaryFormatOptions): string;

    toBinaryString(
        callback: MyLibrary.BinaryFormatCallback,
        opts?: MyLibrary.BinaryFormatOptions
    ): string;
}

/*~ If you need to declare several types, place them inside a namespace
 *~ to avoid adding too many things to the global namespace.
 */
declare namespace MyLibrary {
    type BinaryFormatCallback = (n: number) => string;
    interface BinaryFormatOptions {
        prefix?: string;
        padding: number;
    }
}

global.d.ts

全局代码库

全局代码库可以通过全局作用域来访问(例如,不使用任何形式的import语句)。
许多代码库只是简单地导出一个或多个供使用的全局变量。
比如,如果你使用jQuery,那么可以使用$变量来引用它。

$(() => {
    console.log('hello!');
});

你通常能够在文档里看到如何在 HTML 的 script 标签里引用代码库:

<script src="http://a.great.cdn.for/someLib.js"></script>

目前,大多数流行的全局代码库都以 UMD 代码库发布。
UMD 代码库与全局代码库很难通过文档来识别。
在编写全局代码库的声明文件之前,确保代码库不是 UMD 代码库。

从代码来识别全局代码库

通常,全局代码库的代码十分简单。
一个全局的“Hello, world”代码库可以如下:

function createGreeting(s) {
    return 'Hello, ' + s;
}

或者这样:

window.createGreeting = function (s) {
    return 'Hello, ' + s;
};

在阅读全局代码库的代码时,你会看到:

  • 顶层的var语句或function声明
  • 一个或多个window.someName赋值语句
  • 假设 DOM 相关的原始值documentwindow存在

你不会看到:

  • 检查或使用了模块加载器,如requiredefine
  • CommonJS/Node.js 风格的导入语句,如var fs = require("fs");
  • define(...)调用
  • 描述require或导入代码库的文档

全局代码库的示例

由于将全局代码库转换为 UMD 代码库十分容易,因此很少有代码库仍然使用全局代码库风格。
然而,小型的代码库以及需要使用 DOM 的代码库仍然可以是全局的。

全局代码库模版

你可以看到如下声明文件的示例:

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

/*~ If this library is callable (e.g. can be invoked as myLib(3)),
 *~ include those call signatures here.
 *~ Otherwise, delete this section.
 */
declare function myLib(a: string): string;
declare function myLib(a: number): number;

/*~ If you want the name of this library to be a valid type name,
 *~ you can do so here.
 *~
 *~ For example, this allows us to write 'var x: myLib';
 *~ Be sure this actually makes sense! If it doesn't, just
 *~ delete this declaration and add types inside the namespace below.
 */
interface myLib {
    name: string;
    length: number;
    extras?: string[];
}

/*~ If your library has properties exposed on a global variable,
 *~ place them here.
 *~ You should also place types (interfaces and type alias) here.
 */
declare namespace myLib {
    //~ We can write 'myLib.timeout = 50;'
    let timeout: number;

    //~ We can access 'myLib.version', but not change it
    const version: string;

    //~ There's some class we can create via 'let c = new myLib.Cat(42)'
    //~ Or reference e.g. 'function f(c: myLib.Cat) { ... }
    class Cat {
        constructor(n: number);

        //~ We can read 'c.age' from a 'Cat' instance
        readonly age: number;

        //~ We can invoke 'c.purr()' from a 'Cat' instance
        purr(): void;
    }

    //~ We can declare a variable as
    //~   'var s: myLib.CatSettings = { weight: 5, name: "Maru" };'
    interface CatSettings {
        weight: number;
        name: string;
        tailLength?: number;
    }

    //~ We can write 'const v: myLib.VetID = 42;'
    //~  or 'const v: myLib.VetID = "bob";'
    type VetID = string | number;

    //~ We can invoke 'myLib.checkCat(c)' or 'myLib.checkCat(c, v);'
    function checkCat(c: Cat, s?: VetID);
}

module-class.d.ts

示例,当你想要处理如下的 JavaScriptr 的代码时:

const Greeter = require('super-greeter');

const greeter = new Greeter();
greeter.greet();

能够同时处理UMD导入和模块导入:

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

/*~ This is the module template file for class modules.
 *~ You should rename it to index.d.ts and place it in a folder with the same name as the module.
 *~ For example, if you were writing a file for "super-greeter", this
 *~ file should be 'super-greeter/index.d.ts'
 */

// Note that ES6 modules cannot directly export class objects.
// This file should be imported using the CommonJS-style:
//   import x = require('[~THE MODULE~]');
//
// Alternatively, if --allowSyntheticDefaultImports or
// --esModuleInterop is turned on, this file can also be
// imported as a default import:
//   import x from '[~THE MODULE~]';
//
// Refer to the TypeScript documentation at
// https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require
// to understand common workarounds for this limitation of ES6 modules.

/*~ If this module is a UMD module that exposes a global variable 'myClassLib' when
 *~ loaded outside a module loader environment, declare that global here.
 *~ Otherwise, delete this declaration.
 */
export as namespace myClassLib;

/*~ This declaration specifies that the class constructor function
 *~ is the exported object from the file
 */
export = MyClass;

/*~ Write your module's methods and properties in this class */
declare class MyClass {
    constructor(customGreeting?: string);

    greet: void;

    myMethod(opts: MyClass.MyClassMethodOptions): number;
}

/*~ If you want to expose types from your module as well, you can
 *~ place them in this block.
 *~
 *~ Note that if you decide to include this namespace, the module can be
 *~ incorrectly imported as a namespace object, unless
 *~ --esModuleInterop is turned on:
 *~   import * as x from '[~THE MODULE~]'; // WRONG! DO NOT DO THIS!
 */
declare namespace MyClass {
    export interface MyClassMethodOptions {
        width?: number;
        height?: number;
    }
}

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~]>

/*~ This is the module template file for function modules.
 *~ You should rename it to index.d.ts and place it in a folder with the same name as the module.
 *~ For example, if you were writing a file for "super-greeter", this
 *~ file should be 'super-greeter/index.d.ts'
 */

// Note that ES6 modules cannot directly export class objects.
// This file should be imported using the CommonJS-style:
//   import x = require('[~THE MODULE~]');
//
// Alternatively, if --allowSyntheticDefaultImports or
// --esModuleInterop is turned on, this file can also be
// imported as a default import:
//   import x from '[~THE MODULE~]';
//
// Refer to the TypeScript documentation at
// https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require
// to understand common workarounds for this limitation of ES6 modules.

/*~ If this module is a UMD module that exposes a global variable 'myFuncLib' when
 *~ loaded outside a module loader environment, declare that global here.
 *~ Otherwise, delete this declaration.
 */
export as namespace myFuncLib;

/*~ This declaration specifies that the function
 *~ is the exported object from the file
 */
export = MyFunction;

/*~ This example shows how to have multiple overloads for your function */
declare function MyFunction(name: string): MyFunction.NamedReturnType;
declare function MyFunction(length: number): MyFunction.LengthReturnType;

/*~ If you want to expose types from your module as well, you can
 *~ place them in this block. Often you will want to describe the
 *~ shape of the return type of the function; that type should
 *~ be declared in here, as this example shows.
 *~
 *~ Note that if you decide to include this namespace, the module can be
 *~ incorrectly imported as a namespace object, unless
 *~ --esModuleInterop is turned on:
 *~   import * as x from '[~THE MODULE~]'; // WRONG! DO NOT DO THIS!
 */
declare namespace MyFunction {
    export interface LengthReturnType {
        width: number;
        height: number;
    }
    export interface NamedReturnType {
        firstName: string;
        lastName: string;
    }

    /*~ If the module also has properties, declare them here. For example,
     *~ this declaration says that this code is legal:
     *~   import f = require('myFuncLibrary');
     *~   console.log(f.defaultName);
     */
    export const defaultName: string;
    export let defaultLength: number;
}

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~]>

/*~ This is the module plugin template file. You should rename it to index.d.ts
 *~ and place it in a folder with the same name as the module.
 *~ For example, if you were writing a file for "super-greeter", this
 *~ file should be 'super-greeter/index.d.ts'
 */

/*~ On this line, import the module which this module adds to */
import * as m from 'someModule';

/*~ You can also import other modules if needed */
import * as other from 'anotherModule';

/*~ Here, declare the same module as the one you imported above */
declare module 'someModule' {
    /*~ Inside, add new function, classes, or variables. You can use
     *~ unexported types from the original module if needed. */
    export function theNewMethod(x: m.foo): other.bar;

    /*~ You can also add new properties to existing interfaces from
     *~ the original module by writing interface augmentations */
    export interface SomeModuleOptions {
        someModuleSetting?: string;
    }

    /*~ New types can also be declared and will appear as if they
     *~ are in the original module */
    export interface MyModulePluginOptions {
        size: 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~]>

/*~ This is the module template file. You should rename it to index.d.ts
 *~ and place it in a folder with the same name as the module.
 *~ For example, if you were writing a file for "super-greeter", this
 *~ file should be 'super-greeter/index.d.ts'
 */

/*~ If this module is a UMD module that exposes a global variable 'myLib' when
 *~ loaded outside a module loader environment, declare that global here.
 *~ Otherwise, delete this declaration.
 */
export as namespace myLib;

/*~ If this module has methods, declare them as functions like so.
 */
export function myMethod(a: string): string;
export function myOtherMethod(a: number): number;

/*~ You can declare types that are available via importing the module */
export interface someType {
    name: string;
    length: number;
    extras?: string[];
}

/*~ You can declare properties of the module using const, let, or var */
export const myField: number;

/*~ If there are types, properties, or methods inside dotted names
 *~ of the module, declare them inside a 'namespace'.
 */
export namespace subProp {
    /*~ For example, given this definition, someone could write:
     *~   import { subProp } from 'yourModule';
     *~   subProp.foo();
     *~ or
     *~   import * as yourMod from 'yourModule';
     *~   yourMod.subProp.foo();
     */
    export function foo(): void;
}