TypeScript声明

503 阅读29分钟

如何创建高质量的TypeScript声明文件(一)

库结构

“库结构”可帮助您了解常用库格式以及如何为每种格式编写正确的声明文件。 如果您正在编辑现有文件,则可能不需要阅读这篇文章。 新声明文件的作者必须阅读本篇文章以正确理解库的格式如何影响声明文件的写入。

介绍

从广义上讲,构造声明文件的方式取决于库的使用方式。 有许多方法可以在JavaScript中提供供消费的库,你需要编写你的声明文件来匹配它。 本篇文章介绍了如何识别公共库模式,以及如何编写与该模式对应的声明文件。

每种类型的主要库结构模式在“模板”部分中都有相应的文件。 您可以从这些模板开始,以帮助您更快地前进。

识别各种库的类型

首先,我们将回顾TypeScript声明文件可以表示的库类型。 我们将简要介绍如何使用各种库,如何编写,以及列出现实世界中的一些示例库。

识别库的结构是编写其声明文件的第一步。 我们将根据其用法和代码给出关于如何识别结构的提示。 根据图书馆的文档和组织,一个可能比另一个更容易。 我们建议您使用哪种方式更舒适。

全局库

全局库是可以从全局范围访问的库(即不使用任何形式的导入)。 许多库只是公开一个或多个全局变量以供使用。 例如,如果您使用的是jQuery,只需引用它就可以使用$变量:

您通常会在全局库的文档中看到如何在HTML脚本标记中使用库的指导:

<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语句或函数声明
  • window.someName的一个或多个赋值
  • 存在DOM原语(如文档或窗口)的假设

你不会看到:

  • 检查或使用模块加载器,如require或define
  • CommonJS/Node.js样式导入形式var fs = require("fs");
  • 调用define(...)
  • 描述如何require或import库的文档

全局库的例子

因为将全局库转换为UMD库通常很容易,所以很少有流行的库仍然以全局风格编写。 但是,小而且需要DOM(或没有依赖关系)的库可能仍然是全局的。

全局库模板

模板文件global.d.ts定义了一个示例库myLib。 请务必阅读"防止名称冲突"脚注。

如何创建高质量的TypeScript声明文件(二)

继续上篇文章[如何创建高质量的TypeScript声明文件(一)]

模块化库

有些库只能在模块加载器环境中工作。 例如,因为express仅适用于Node.js,必须使用CommonJS require函数加载。

ECMAScript 2015(也称为ES2015,ECMAScript 6和ES6),CommonJS和RequireJS具有类似的导入模块的概念。 例如,在JavaScript CommonJS(Node.js)中,您可以编写

var fs = require("fs");

在TypeScript或ES6中,import关键字用于相同的目的:

import fs = require("fs");

您通常会看到模块化库在其文档中包含以下行之一:

var someLib = require('someLib');

define(..., ['someLib'], function(someLib) {

});

与全局模块一样,您可能会在UMD模块的文档中看到这些示例,因此请务必查看代码或文档。

从代码中识别模块库

模块化库通常至少具有以下某些功能:

  • 无条件调用require或define
  • import * as a from 'b'的声明,或export c;
  • 对exports或module.exports的赋值

他们很少会:

  • 分配window或global的属性

模块化库的示例

许多流行的Node.js库都在模块系列中,例如express,gulp和request。

UMD

UMD模块可以用作模块(通过导入),也可以用作全局模块(在没有模块加载器的环境中运行)。 许多流行的库,如Moment.js,都是以这种方式编写的。 例如,在Node.js中或使用RequireJS,您可以编写:

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

而在vanilla浏览器环境中,你会写:

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 define,typeof window或typeof module的测试,特别是在文件的顶部,它几乎总是一个UMD库。

UMD库的文档通常还会演示一个"Using in Node.js"示例,其中显示了require,以及一个"Using in the browser"示例,该示例显示了使用<script>标记加载脚本。

UMD库的示例

大多数流行的库现在都可以作为UMD包使用。 示例包括jQuery,Moment.js,lodash等等。

模板

模块有三个模板,module.d.ts,module-class.d.ts和module-function.d.ts。

如果您的模块可以像函数一样被调用,请使用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");

同样的脚注适用于这些模块。

如果您的模块不可调用或可构造,请使用module.d.ts文件。

如何创建高质量的TypeScript声明文件(三)

继续上篇文章[如何创建高质量的TypeScript声明文件(二)]

模块插件或UMD插件

模块插件更改另一个模块(UMD或模块)的形状。 例如,在Moment.js中,时刻范围为时刻对象添加了一个新的范围方法。

出于编写声明文件的目的,无论要更改的模块是普通模块还是UMD模块,您都将编写相同的代码。

模板

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

全局插件

全局插件是改变某些全局形状的全局代码。 与全局修改模块一样,这些会增加运行时冲突的可能性。

例如,某些库将新函数添加到Array.prototype或String.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;

如何创建高质量的TypeScript声明文件(四)

继续上篇文章[如何创建高质量的TypeScript声明文件(三)]

对UMD库的依赖性

来自全局库

如果您的全局库依赖于UMD模块,请使用/// <reference types指令

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

function getThing(): moment;

来自模块或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)只能具有属性; 顶级模块对象永远不可调用。 这里最常见的解决方案是为可调用/可构造对象定义默认导出; 某些模块加载程序填充程序将自动检测此情况并使用默认导出替换顶级对象。

如何创建高质量的TypeScript声明文件(五) - 示例

前面四篇文章一起介绍了在声明文件中关于库结构的一些介绍,本篇文章之后分享一些API的文档,还有它们的使用示例,并且阐述如何为他们创建声明文件

这些示例以大致递增的复杂度顺序排序。

  • 全局变量
  • 全局函数
  • 具有属性的对象
  • 重载函数
  • 可重用类型(接口)
  • 可重用类型(类型别名)
  • 组织类型

示例

全局变量

文档

全局变量foo包含存在的小部件数。

代码

console.log("Half the number of widgets is " + (foo / 2));

声明

使用declare var来声明变量。如果变量是只读的,则可以使用declare const。如果变量是块作用域的,您也可以使用declare let。

/** The number of widgets present */
declare var foo: number;

全局函数

文档

您可以使用字符串调用函数greet来向用户显示问候语。

代码

greet("hello, world");

声明

使用declare function声明函数。

declare function greet(greeting: string): void;

具有属性的对象

文档

全局变量myLib有一个用于创建问候语的makeGreeting函数,以及一个属性numberOfGreetings,用于指示到目前为止所做的问候数。

代码

let result = myLib.makeGreeting("hello, world");
console.log("The computed greeting is:" + result);

let count = myLib.numberOfGreetings;

声明

使用declare namespace描述由点式表示法访问的类型或值。

declare namespace myLib {
    function makeGreeting(s: string): string;
    let numberOfGreetings: number;
}

重载函数

文档

getWidget函数接受一个数字并返回一个Widget,或者接受一个字符串并返回一个Widget数组。

代码

let x: Widget = getWidget(43);

let arr: Widget[] = getWidget("all of them");

声明

declare function getWidget(n: number): Widget;
declare function getWidget(s: string): Widget[];

可重用类型(接口)

文档

指定问候语时,必须传递GreetingSettings对象。该对象具有以下属性:

1 - 问候语:必填字符串 2 - 持续时间:可选的时间长度(以毫秒为单位) 3 - 颜色:可选字符串,例如"#FF00FF"

代码

greet({
  greeting: "hello world",
  duration: 4000
});

声明

使用接口定义具有属性的类型。

interface GreetingSettings {
  greeting: string;
  duration?: number;
  color?: string;
}

declare function greet(setting: GreetingSettings): void;

如何创建高质量的TypeScript声明文件(六) - 示例

继续上篇文章【如何创建高质量的TypeScript声明文件(五) - 示例】 上篇文章介绍了

  • 全局变量
  • 全局函数
  • 具有属性的对象
  • 重载函数
  • 可重用类型(接口)

几种示例

下面继续分享剩余的几种示例

  • 可重用类型(类型别名)
  • 组织类型

可重用类型(类型别名)

文档

在需要问候语的任何地方,您可以提供字符串,返回字符串的函数或Greeter实例。

代码

function getGreeting() {
    return "howdy";
}
class MyGreeter extends Greeter { }

greet("hello");
greet(getGreeting);
greet(new MyGreeter());

声明

您可以使用类型别名来为类型创建简写:

type GreetingLike = string | (() => string) | MyGreeter;

declare function greet(g: GreetingLike): void;

组织类型

文档

greeter对象可以记录到文件或显示警报。 您可以向.log(...)提供LogOptions,并为.alert(...)提供警报选项

代码

const g = new Greeter("Hello");
g.log({ verbose: true });
g.alert({ modal: false, title: "Current Greeting" });

声明

使用命名空间来组织类型。

declare namespace GreetingLib {
    interface LogOptions {
        verbose?: boolean;
    }
    interface AlertOptions {
        modal: boolean;
        title?: string;
        color?: string;
    }
}

您还可以在一个声明中创建嵌套的命名空间:

declare namespace GreetingLib.Options {
    // Refer to via GreetingLib.Options.Log
    interface Log {
        verbose?: boolean;
    }
    interface Alert {
        modal: boolean;
        title?: string;
        color?: string;
    }
}

具有属性的对象

文档

您可以通过实例化Greeter对象来创建一个greeter,或者通过从中扩展来创建一个自定义的greeter。

代码

const myGreeter = new Greeter("hello, world");
myGreeter.greeting = "howdy";
myGreeter.showGreeting();

class SpecialGreeter extends Greeter {
    constructor() {
        super("Very special greetings");
    }
}

声明

使用declare类来描述类或类类对象。 类可以具有属性和方法以及构造函数。

declare class Greeter {
    constructor(greeting: string);

    greeting: string;
    showGreeting(): void;
}

如何创建高质量的TypeScript声明文件(七) - 该做什么和不该做什么

该做什么和不该做什么

一般类型

数字,字符串,布尔值和对象

不要使用Number,String,Boolean或Object类型。 这些类型指的是在JavaScript代码中几乎从不正确使用的非原始盒装对象。

/* WRONG */
function reverse(s: String): String;

请使用类型number,string和boolean。

/* OK */
function reverse(s: string): string;

而不是Object,使用非基本对象类型(在TypeScript 2.2中添加)。

泛型

不要使用不使用其类型参数的泛型类型。在TypeScript FAQ页面中查看更多详细信息。

回调类型

返回回调类型

不要将返回类型any用于其值将被忽略的回调:

/* WRONG */
function fn(x: () => any) {
    x();
}

对于其值将被忽略的回调,请使用返回类型void:

/* OK */
function fn(x: () => void) {
    x();
}

原因:使用void更安全,因为它可以防止您以未经检查的方式意外使用x的返回值:

function fn(x: () => void) {
    var k = x(); // oops! meant to do something else
    k.doSomething(); // error, but would be OK if the return type had been 'any'
}

回调中的可选参数

除非你真的是这样说,否则不要在回调中使用可选参数:

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

这具有非常具体的含义:完成的回调可以使用1个参数调用,也可以使用2个参数调用。作者可能打算说回调可能不关心elapsedTime参数,但是没有必要使参数可选来完成这一点 - 提供一个接受较少参数的回调总是合法的。

写回调参数是非可选的:

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

重载和回调

不要编写仅在回调函数参数上不同的单独重载:

/* WRONG */
declare function beforeAll(action: () => void, timeout?: number): void;
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;

应该只使用最大参数个数写一个重载:

/* OK */
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;

原因:忽略参数的回调总是合法的,因此不需要更短的过载。首先提供较短的回调允许传入错误输入的函数,因为它们匹配第一个重载。

函数重载

顺序

不要在更具体的重载之前放置更多的一般重载:

/* WRONG */
declare function fn(x: any): any;
declare function fn(x: HTMLElement): number;
declare function fn(x: HTMLDivElement): string;

var myElem: HTMLDivElement;
var x = fn(myElem); // x: any, wat?

通过在更具体的签名之后放置更一般的签名来对重载进行排序:

/* OK */
declare function fn(x: HTMLDivElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: any): any;

var myElem: HTMLDivElement;
var x = fn(myElem); // x: string, :)

原因:TypeScript在解析函数调用时选择第一个匹配的重载。当较早的重载比较晚的重载“更一般”时,后一个重载是有效隐藏的,不能被调用。

使用可选参数

不要写几个仅在尾随参数上有所不同的重载:

/* WRONG */
interface Example {
    diff(one: string): number;
    diff(one: string, two: string): number;
    diff(one: string, two: string, three: boolean): number;
}

尽可能使用可选参数:

/* OK */
interface Example {
    diff(one: string, two?: string, three?: boolean): number;
}

请注意,只有当所有重载具有相同的返回类型时,才会发生此折叠。

原因:这有两个重要原因。

TypeScript通过查看是否可以使用源的参数调用目标的任何签名来解析签名兼容性,并允许使用无关的参数。例如,只有在使用可选参数正确编写签名时,此代码才会公开错误:

function fn(x: (a: string, b: number, c: number) => void) { }
var x: Example;
// When written with overloads, OK -- used first overload
// When written with optionals, correctly an error
fn(x.diff);

第二个原因是消费者使用TypeScript的“严格空检查”功能。由于未指定的参数在JavaScript中显示为未定义,因此将显式的undefined传递给带有可选参数的函数通常很好。例如,在严格的空值下,此代码应该是正常的:

使用联合类型

不要只在一个参数位置写入因类型不同的重载:

/* WRONG */
interface Moment {
    utcOffset(): number;
    utcOffset(b: number): Moment;
    utcOffset(b: string): Moment;
}

尽可能使用联合类型:

/* OK */
interface Moment {
    utcOffset(): number;
    utcOffset(b: number|string): Moment;
}

请注意,我们在这里没有使b可选,因为签名的返回类型不同。

原因:这对于将“值”传递给函数的人来说非常重要:

function fn(x: string): void;
function fn(x: number): void;
function fn(x: number|string) {
    // When written with separate overloads, incorrectly an error
    // When written with union types, correctly OK
    return moment().utcOffset(x);
}

如何创建高质量的TypeScript声明文件(八) - 深入

深入

定义文件原理:深入

构建模块以提供您想要的精确API形状可能会非常棘手。例如,我们可能想要一个可以使用或不使用new调用的模块来生成不同的类型,在层次结构中公开各种命名类型,并且在模块对象上也有一些属性。

通过深入理解定义文件原理,您将拥有编写复杂定义文件的工具,这些文件可以显示友好的API表面。本指南重点介绍模块(或UMD)库,因为此处的选项更加多样化。

关键概念

通过了解TypeScript如何工作的一些关键概念,您可以完全理解如何进行任何形式的定义。

类型

为了更明确,引入了一种类型:

  • 类型别名声明(type sn = number | string;)
  • 接口声明(interface I { x: number[]; })
  • 类声明(class C { })
  • 枚举声明(enum E { A, B, C })
  • 引用类型的import声明

每个声明表单都会创建一个新的类型名称。

与类型一样,您可能已经了解了什么是值。值是我们可以在表达式中引用的运行时名称。例如,让x = 5;创建一个名为x的值。

同样,明确地,以下事物创造价值:

  • let,const和var声明
  • 包含值的命名空间或模块声明
  • 枚举声明
  • 一个类声明
  • 引用值的导入声明
  • 功能声明

命令空间

类型可以存在于名称空间中。例如,如果我们有声明let x: A.B.C,我们说类型C来自A.B命名空间。

这种区别是微妙而重要的 - 在这里,A.B不一定是一种类型或价值。

简单组合:一个名称,多个含义

给定名称A,我们可能会为A找到最多三种不同的含义:类型,值或命名空间。如何解释名称取决于使用它的上下文。例如,在声明中,让m:A.A = A;,A首先用作命名空间,然后用作类型名称,然后用作值。这些含义最终可能指的是完全不同的声明!

这可能看起来令人困惑,但只要我们不过度超载事物,它实际上非常方便。让我们看看这种组合行为的一些有用方面。

内置组合

精明的读者会注意到,例如,类出现在类型和值列表中。声明类C {}创建两件事:一个C类引用类的实例形状,一个值C引用类的构造函数。枚举声明的行为类似。

用户组合

假设我们写了一个模块文件foo.d.ts:

export var SomeVar: { a: SomeType };
export interface SomeType {
  count: number;
}

然后调用它:

import * as foo from './foo';
let x: foo.SomeType = foo.SomeVar.a;
console.log(x.count);

这种方法效果很好,但我们可以想象SomeType和SomeVar非常密切相关,因此您希望它们具有相同的名称。我们可以使用组合来在同一个名称Bar下呈现这两个不同的对象(值和类型):

export var Bar: { a: Bar };
export interface Bar {
  count: number;
}

这为使用代码中的解构提供了一个非常好的机会:

···
import { Bar } from './foo';
let x: Bar = Bar.a;
console.log(x.count);
···

同样,我们在这里使用Bar作为类型和值。请注意,我们不必将Bar值声明为Bar类型 - 它们是独立的。

高级组合

某些类型的声明可以跨多个声明组合。例如,类C {}和接口C {}可以共存,并且都为C类型提供属性。

只要它不会产生冲突,这是合法的。一般的经验法则是值总是与同名的其他值冲突,除非它们被声明为名称空间,如果使用类型别名声明(type s = string)声明类型,则类型将发生冲突,并且名称空间永远不会发生冲突。

让我们看看如何使用它。

使用interface添加

我们可以使用另一个接口声明向接口添加其他成员:

interface Foo {
  x: number;
}
// ... elsewhere ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

这也适用于类:

class Foo {
  x: number;
}
// ... elsewhere ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

请注意,我们无法使用接口添加类型别名(type s = string;)。

使用namespace添加

命名空间声明可用于以任何不会产生冲突的方式添加新类型,值和命名空间。

例如,我们可以向类添加静态成员:

class C {
}
// ... elsewhere ...
namespace C {
  export let x: number;
}
let y = C.x; // OK

请注意,在此示例中,我们向C的静态端(其构造函数)添加了一个值。这是因为我们添加了一个值,所有值的容器都是另一个值(类型由名称空间包含,名称空间包含在其他名称空间中)。

我们还可以为类添加命名空间类型:

class C {
}
// ... elsewhere ...
namespace C {
  export interface D { }
}
let y: C.D; // OK

在这个例子中,在我们为它编写名称空间声明之前,没有名称空间C.C作为命名空间的含义与类创建的C的值或类型含义不冲突。

最后,我们可以使用命名空间声明执行许多不同的合并。这不是一个特别现实的例子,但显示了各种有趣的行为:

namespace X {
  export interface Y { }
  export class Z { }
}

// ... elsewhere ...
namespace X {
  export var Y: number;
  export namespace Z {
    export class C { }
  }
}
type X = string;

在此示例中,第一个块创建以下名称含义:

  • 值X(因为名称空间声明包含值Z)
  • 命名空间X(因为命名空间声明包含一个类型,Y)
  • X命名空间中的类型Y.
  • X命名空间中的类型Z(类的实例形状)
  • 值Z,它是X值的属性(类的构造函数)

第二个块创建以下名称含义:

  • 值Y(类型编号),它是X值的属性
  • 命名空间Z.
  • 值Z,它是X值的属性
  • X.Z命名空间中的类型C.
  • 值C,它是X.Z值的属性
  • X型

使用export =或import

一个重要的规则是export和import声明export或import其目标的所有含义。

如何创建高质量的TypeScript声明文件(九) - 模板

模板

综合前面的分享,这里分享下官方的模板,方便后面实战中随机查看

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

/*~ 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

// 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 to 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

// 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

// 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('someLibrary');
 *~
 *~ Refer to the documentation 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(someParam?: string);

    someProperty: string[];

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

/*~ If you want to expose types from your module as well, you can
 *~ place them in this block.
 */
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 callable functions.
 *~ This file should be imported using the CommonJS-style:
 *~   import x = require('someLibrary');
 *~
 *~ Refer to the documentation 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.
 */
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;
}

如何创建高质量的TypeScript声明文件(十) - 发布

发布

经过前面的文章介绍声明文件的使用,现在您应该可以创作一个声明文件了,并且可以将创作的文件发布到npm了。发布的话可以通过两种主要方式将声明文件发布到npm:

  1. 打包你的npm包
  2. 在npm上发布到@types组织

如果你的包是用TypeScript编写的,那么第一种方法是受欢迎的。 使用--declaration标志生成声明文件。 这样,您的声明和JavaScript始终保持同步。

如果您的包不是用TypeScript编写的,那么第二种方法是首选方法。

在你的npm包中包含声明

如果你的包有一个main.js文件,你还需要在package.json文件中指明主声明文件。 将types属性设置为指向打包的声明文件。 例如:

{
    "name": "awesome",
    "author": "Vandelay Industries",
    "version": "1.0.0",
    "main": "./lib/main.js",
    "types": "./lib/main.d.ts"
}

请注意,“typings”字段与“types”同义,也可以使用。

另请注意,如果主声明文件名为index.d.ts并且位于包的根目录(index.js旁边),则不需要标记“types”属性,但建议这样做。

依赖

所有依赖项都由npm管理。 确保您所依赖的所有声明包都在package.json的“dependencies”部分中进行了适当标记。 例如,假设我们编写了一个使用Browserify和TypeScript的包。

{
    "name": "browserify-typescript-extension",
    "author": "Vandelay Industries",
    "version": "1.0.0",
    "main": "./lib/main.js",
    "types": "./lib/main.d.ts",
    "dependencies": {
        "browserify": "latest",
        "@types/browserify": "latest",
        "typescript": "next"
    }
}

在这里,我们的包依赖于browserify和typescript包。 browserify不会将其声明文件与其npm包捆绑在一起,因此我们需要依赖@types/browserify来声明它的声明。 另一方面,typescript打包其声明文件,因此不需要任何其他依赖项。

我们的包公开了每个声明的声明,因此我们的browserify-typescript-extension包的任何用户也需要具有这些依赖关系。 出于这个原因,我们使用“依赖”而不是“devDependencies”,因为否则我们的消费者需要手动安装这些包。 如果我们刚刚编写了一个命令行应用程序并且不希望我们的包被用作库,那么我们可能已经使用了devDependencies。

警告

/// <reference path="..." />

不要在声明文件中使用/// <reference path =“...”/>

/// <reference path="../typescript/lib/typescriptServices.d.ts" />
....

使用/// <reference types="..." />替换

/// <reference types="typescript" />
....

包装依赖声明

如果您的类型定义依赖于另一个包:

  • 不要将它与你的相结合,将每个文件保存在自己的文件中。
  • 不要复制包中的声明。
  • 如果它没有打包其声明文件,请依赖于npm类型声明包。

发布到@types

@types组织下的软件包将使用types-publisher工具从DefinitelyTyped自动发布。要将您的声明作为@types包发布,请向 github.com/DefinitelyT… 提交拉取请求。您可以在贡献指南页面中找到更多详细信息。

如何创建高质量的TypeScript声明文件(十一) - 使用

使用

在TypeScript 2.0中,在获取,使用和查找声明文件时,它变得非常容易。 下面说下如何做到这三点。

下载

在TypeScript 2.0及更高版本中获取类型声明不需要除npm之外的任何工具。

例如,获取像lodash这样的库的声明只需要以下命令

npm install --save @types/lodash

值得注意的是,如果npm包已经包含了发布中描述的声明文件,则不需要下载相应的@types包。

使用

从那里你可以在你的TypeScript代码中使用lodash而不用大惊小怪。这适用于模块和全局代码。

例如,一旦你安装了你的类型声明,就可以使用import和write

import * as _ from "lodash";
_.padStart("Hello TypeScript!", 20, " ");

或者如果您不使用模块,则可以使用全局变量_。

_.padStart("Hello TypeScript!", 20, " ");

搜索

在大多数情况下,类型声明包应始终与npm上的包名称相同,但前缀为@types/,但如果需要,可以查看aka.ms/types 以查找你最喜欢的库的包。

注意:如果您要搜索的声明文件不存在,您可以随时贡献一份,并帮助下一位寻找它的开发人员。 有关详细信息,请参阅DefinitelyTyped贡献指南页面。