【HarmonyOS NEXT鸿蒙系统应用开发TypeScript(TS)开发基础教程】-第26课时-TypeScript 声明文件详细介绍
一般来讲,你组织声明文件的方式取决于库是如何被使用的。在JavaScript里提供了很多库的使用方法,这就需要你书写声明文件去匹配它们。这篇指南涵盖了如何识别常见库的模式,与怎么样书写符合相应模式的声明文件。针对每种主要的库的组织模式,在模版一节都有对应的文件。你可以利用它们帮助你快速上手。这篇指南的目的是教你如何书写高质量的TypeScript声明文件。我们在这里会展示一些API的文档,还有它们
TypeScript 声明文件介绍
篇指南的目的是教你如何书写高质量的TypeScript声明文件。
在这篇指南里,我们假设你对TypeScript已经有了基本的了解。 如果没有,请先阅读 TypeScript手册 来了解一些基本知识,尤其是类型和命名空间部分。
章节
这篇指南被分成了以下章节。
结构
结构一节将帮助你了解常见库的格式以及如何为每种格式书写正确的声明文件。 如果你在编辑一个已经存在的文件,那么你可能不需要阅读此章节。 如果你在书写新的声明文件,那么你必须阅读此章节以理解库的不同格式是如何影响声明文件的书写的。
规范
声明文件里有很多常见的错误是很容易避免的。规范一节指出了常见的错误, 描述了如何发现它们, 与怎样去修复。 每个人都要阅读这个章节以了解如何避免常见错误。
举例
很多时候,我们只能通过一些示例来了解第三方库是如何工作的,同时我们需要为这样的库书写声明文件。举例一节展示了很多常见的API模式以及如何为它们书写声明文件。 这篇指南是针对TypeScript初学者的,他们可能还不了解TypeScript里的所有语言结构。
深入
对于那些对声明文件底层工作机制感兴趣的老手们,深入一节解释了很多高级书写声明文件的高级概念, 以及展示了如何利用这些概念来创建整洁和直观的声明文件。
模版
在模版一节里,你能找到一些声明文件,它们可以帮助你快速开始 当你在书写一个新声明文件的时候。 参考结构这篇文档来找到应该使用哪个模版文件。
发布到npm
发布一节讲解了如何发布声明文件为npm包,及如何管理包的依赖。
查找与安装声明文件
对于JavaScript库的使用者来讲,使用一节提供了一些简单步骤来定位与安装相应的声明文件。
TypeScript 声明文件结构
概述
一般来讲,你组织声明文件的方式取决于库是如何被使用的。 在JavaScript里提供了很多库的使用方法,这就需要你书写声明文件去匹配它们。 这篇指南涵盖了如何识别常见库的模式,与怎么样书写符合相应模式的声明文件。
针对每种主要的库的组织模式,在模版一节都有对应的文件。 你可以利用它们帮助你快速上手。
识别库的类型
首先,我们先看一下TypeScript声明文件能够表示的库的类型。 这里会简单展示每种类型的库的使用方式,如何去书写,还有一些真实案例。
识别库的类型是书写声明文件的第一步。 我们将会给出一些提示,关于怎样通过库的 使用方法及其源码来识别库的类型。 根据库的文档及组织结构不同,这两种方式可能一个会比另外的那个简单一些。 我们推荐你使用任意你喜欢的方式。
全局库
全局库是指能在全局命名空间下访问的(例如:不需要使用任何形式的import
)。 许多库都是简单的暴露出一个或多个全局变量。 比如,如果你使用过 jQuery,$
变量可以被够简单的引用:
$(() => { console.log('hello!'); } );
你经常会在全局库的指南文档上看到如何在HTML里用脚本标签引用库:
<script src="http://a.great.cdn.for/someLib.js" rel="external nofollow" ></script>
目前,大多数流行的全局访问型库实际上都以UMD库的形式进行书写(见后文)。 UMD库的文档很难与全局库文档两者之间难以区分。 在书写全局声明文件前,一定要确认一下库是否真的不是UMD。
从代码上识别全局库
全局库的代码通常都十分简单。 一个全局的“Hello, world”库可能是这样的:
function createGreeting(s) {
return "Hello, " + s;
}
或这样:
window.createGreeting = function(s) {
return "Hello, " + s;
}
当你查看全局库的源代码时,你通常会看到:
- 顶级的
var
语句或function
声明 - 一个或多个赋值语句到
window.someName
- 假设DOM原始值像
document
或window
是存在的
你不会看到:
- 检查是否使用或如何使用模块加载器,比如
require
或define
- CommonJS/Node.js风格的导入如
var fs = require("fs");
define(...)
调用- 文档里说明了如果
require
或导入这个库
全局库的例子
由于把一个全局库转变成UMD库是非常容易的,所以很少流行的库还再使用全局的风格。 然而,小型的且需要DOM(或 没有依赖)的库可能还是全局类型的。
全局库模版
模版文件global.d.ts定义了myLib
库作为例子。 一定要阅读 "防止命名冲突"补充说明。
模块化库
一些库只能工作在模块加载器的环境下。 比如,像 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';
orexport 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());
然而在纯净的浏览器环境里你也可以这样写:
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库的文档里经常会包含通过require
“在Node.js里使用”例子, 和“在浏览器里使用”的例子,展示如何使用<script>
标签去加载脚本。
UMD库的例子
大多数流行的库现在都能够被当成UMD包。 比如 jQuery,Moment.js,lodash和许多其它的。
模版
针对模块有三种可用的模块, module.d.ts, module-class.d.ts and module-function.d.ts.
使用module-function.d.ts,如果模块能够作为函数调用。
var x = require("foo");
// Note: calling 'x' as a function
var y = x(42);
一定要阅读补充说明: “ES6模块调用签名的影响”
使用module-class.d.ts如果模块能够使用new
来构造:
var x = require("bar");
// Note: using 'new' operator on the imported variable
var y = new x("hello");
相同的补充说明作用于这些模块。
如果模块不能被调用或构造,使用module.d.ts文件。
模块插件或UMD插件
一个模块插件可以改变一个模块的结构(UMD或模块)。 例如,在Moment.js里, moment-range
添加了新的range
方法到monent
对象。
对于声明文件的目标,我们会写相同的代码不论被改变的模块是一个纯粹的模块还是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;
依赖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和其它加载器里是允许的,ES模块被当作是不可改变的因此这种模式就不可行了。 因为TypeScript是能不预知加载器类型的,所以没在编译时保证,但是开发者如果要转到ES6模块加载器上应该注意这一点。
ES6模块调用签名的影响
很多流行库,比如Express,暴露出自己作为可以调用的函数。 比如,典型的Express使用方法如下:
import exp = require("express");
var app = exp();
在ES6模块加载器里,顶层的对象(这里以exp
导入)只能具有属性; 顶层的模块对象 永远不能被调用。 十分常见的解决方法是定义一个 default
导出到一个可调用的/可构造的对象; 一会模块加载器助手工具能够自己探测到这种情况并且使用 default
导出来替换顶层对象。
TypeScript 声明文件举例
简介
这篇指南的目的是教你如何书写高质量的TypeScript声明文件。 我们在这里会展示一些API的文档,还有它们的使用示例, 并且阐述了如何为它们书写声明文件。
这些例子是按复杂度递增的顺序组织的。
例子
全局变量
文档
全局变量
foo
包含了存在组件总数。
代码
console.log("Half the number of widgets is " + (foo / 2));
声明
使用declare var
声明变量。 如果变量是只读的,那么可以使用 declare const
。 你还可以使用 declare let
如果变量拥有块级作用域。
/** 组件总数 */
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
函数接收一个数字,返回一个组件,或接收一个字符串并返回一个组件数组。
代码
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
对象。 这个对象具有以下几个属性:
- greeting:必需的字符串
- duration: 可靠的时长(毫秒表示)
- color: 可选字符串,比如‘#ff00ff’
代码
greet({
greeting: "hello world",
duration: 4000
});
声明
使用interface
定义一个带有属性的类型。
interface GreetingSettings {
greeting: string;
duration?: number;
color?: string;
}
declare function greet(setting: GreetingSettings): void;
可重用类型(类型别名)
文档
在任何需要欢迎词的地方,你可以提供一个
string
,一个返回string
的函数或一个Greeter
实例。
代码
function getGreeting() {
return "howdy";
}
class MyGreeter extends Greeter { }
greet("hello");
greet(getGreeting);
greet(new MyGreeter());
声明
你可以使用类型别名来定义类型的短名:
type GreetingLike = string | (() => string) | Greeting;
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
对象来自定义欢迎词。
代码
const myGreeter = new Greeter("hello, world");
myGreeter.greeting = "howdy";
myGreeter.showGreeting();
class SpecialGreeter extends Greeter {
constructor() {
super("Very special greetings");
}
}
声明
使用declare class
描述一个类或像类一样的对象。 类可以有属性和方法,就和构造函数一样。
declare class Greeter {
constructor(greeting: string);
greeting: string;
showGreeting(): void;
}
TypeScript 声明文件规范
TypeScript声明文件规范
普通类型
Number
,String
,Boolean
和Object
不要使用如下类型Number
,String
,Boolean
或Object
。 这些类型指的是非原始的装盒对象,它们几乎没在JavaScript代码里正确地使用过。
/* 错误 */
function reverse(s: String): String;
应该使用类型number
,string
,and boolean
。
/* OK */
function reverse(s: string): string;
如果你就要使用Object
类型,考虑使用any
代替。 目前在TypeScript里无法指定一个对象“不是一个原始值”。
泛型
不要定义一个从来没使用过其类型参数的泛型类型。 了解详情 TypeScript FAQ page。
回调函数类型
回调函数返回值类型
不要为返回值被忽略的回调函数设置一个any
类型的返回值类型:
/* 错误 */
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'
}
回调函数里的可选参数
不要在回调函数里使用可选参数除非你真的要这么做:
/* 错误 */
interface Fetcher {
getObject(done: (data: any, elapsedTime?: number) => void): void;
}
这里有一种特殊的意义:done
回调函数可能以1个参数或2个参数调用。 代码大概的意思是说这个回调函数不在乎是否有 elapsedTime
参数, 但是不需要把这个参数当成可选参数来达到此目的 -- 因为总是允许提供一个接收较少参数的回调函数。
应该写出回调函数的非可选参数:
/* OK */
interface Fetcher {
getObject(done: (data: any, elapsedTime: number) => void): void;
}
重载与回调函数
不要因为回调函数参数个数不同而写不同的重载:
/* 错误 */
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;
为什么:回调函数总是可以忽略某个参数的,因此没必要为参数少的情况写重载。 参数少的回调函数首先允许错误类型的函数被传入,因为它们匹配第一个重载。
函数重载
顺序
不要把一般的重载放在精确的重载前面:
/* 错误 */
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会选择第一个匹配到的重载当解析函数调用的时候。 当前面的重载比后面的“普通”,那么后面的被隐藏了不会被调用。
使用可选参数
不要为仅在末尾参数不同时写不同的重载:
/* 错误 */
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解析签名兼容性时会查看是否某个目标签名能够使用源的参数调用, 且允许外来参数。 下面的代码暴露出一个bug,当签名被正确的使用可选参数书写时:
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“严格检查null”特性时。 因为没有指定的参数在JavaScript里表示为undefined
,通常显示地为可选参数传入一个undefined
。 这段代码在严格null模式下可以工作:
var x: Example;
// When written with overloads, incorrectly an error because of passing 'undefined' to 'string'
// When written with optionals, correctly OK
x.diff("something", true ? undefined : "hour");
使用联合类型
不要为仅在某个位置上的参数类型不同的情况下定义重载:
/* WRONG */
interface Moment {
utcOffset(): number;
utcOffset(b: number): Moment;
utcOffset(b: string): Moment;
}
应该尽可能使用联合类型:
/* OK */
interface Moment {
utcOffset(): number;
utcOffset(b: number|string): Moment;
}
注意我们没有让b
成为可选的,因为签名的返回值类型不同。
为什么:This is important for people who are "passing through" a value to your function:
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是如何工作的核心概念, 那么你就能够为任何结构书写声明文件。
类型
如果你正在阅读这篇指南,你可能已经大概了解TypeScript里的类型指是什么。 明确一下, 类型通过以下方式引入:
- 类型别名声明(
type sn = number | string;
) - 接口声明(
interface I { x: number[]; }
) - 类声明(
class C { }
) - 枚举声明(
enum E { A, B, C }
) - 指向某个类型的
import
声明
以上每种声明形式都会创建一个新的类型名称。
值
与类型相比,你可能已经理解了什么是值。 值是运行时名字,可以在表达式里引用。 比如 let x = 5;
创建一个名为x
的值。
同样,以下方式能够创建值:
let
,const
,和var
声明- 包含值的
namespace
或module
声明 enum
声明class
声明- 指向值的
import
声明 function
声明
命名空间
类型可以存在于命名空间里。 比如,有这样的声明 let x: A.B.C
, 我们就认为 C
类型来自A.B
命名空间。
这个区别虽细微但很重要 -- 这里,A.B
不是必需的类型或值。
简单的组合:一个名字,多种意义
一个给定的名字A
,我们可以找出三种不同的意义:一个类型,一个值或一个命名空间。 要如何去解析这个名字要看它所在的上下文是怎样的。 比如,在声明 let m: A.A = A;
, A
首先被当做命名空间,然后做为类型名,最后是值。 这些意义最终可能会指向完全不同的声明!
这看上去另人迷惑,但是只要我们不过度的重载这还是很方便的。 下面让我们来看看一些有用的组合行为。
内置组合
眼尖的读者可能会注意到,比如,class
同时出现在类型和值列表里。 class 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
类型 -- 它们是独立的。
高级组合
有一些声明能够通过多个声明组合。 比如, class C { }
和interface C { }
可以同时存在并且都可以做为C
类型的属性。
只要不产生冲突就是合法的。 一个普通的规则是值总是会和同名的其它值产生冲突除非它们在不同命名空间里, 类型冲突则发生在使用类型别名声明的情况下( type s = string
), 命名空间永远不会发生冲突。
让我们看看如何使用。
利用interface
添加
我们可以使用一个interface
往别一个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
添加
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
在这个例子里,直到我们写了namespace
声明才有了命名空间C
。 做为命名空间的 C
不会与类创建的值C
或类型C
相互冲突。
最后,我们可以进行不同的合并通过namespace
声明。 Finally, we could perform many different merges usingnamespace
declarations. This isn't a particularly realistic example, but shows all sorts of interesting behavior:
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
(因为namespace
声明包含一个值,Z
) - 一个命名空间
X
(因为namespace
声明包含一个值,Z
) - 在命名空间
X
里的类型Y
- 在命名空间
X
里的类型Z
(类的实例结构) - 值
X
的一个属性值Z
(类的构造函数)
第二个代码块创建了以下名字与含义:
- 值
Y
(number
类型),它是值X
的一个属性 - 一个命名空间
Z
- 值
Z
,它是值X
的一个属性 - 在
X.Z
命名空间下的类型C
- 值
X.Z
的一个属性值C
- 类型
X
使用export =
或import
一个重要的原则是export
和import
声明会导出或导入目标的所有含义。
TypeScript 声明文件模板
- global-modifying-module.d.ts
- global-plugin.d.ts
- global.d.ts
- module-class.d.ts
- module-function.d.ts
- module-plugin.d.ts
- module.d.ts
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:
- 与你的npm包捆绑在一起,或
- 发布到npm上的@types organization。
如果你能控制要使用你发布的声明文件的那个npm包的话,推荐第一种方式。 这样的话,你的声明文件与JavaScript总是在一起传递。
包含声明文件到你的npm包
如果你的包有一个主.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
相反,它把声明文件放在了npm包里,因此我们不需要依赖额外的包。
我们的包要从这两个包里暴露出声明文件,因此browserify-typescript-extension
的用户也需要这些依赖。 正因此,我们使用 "dependencies"
而不是"devDependencies"
,否则用户将需要手动安装那些包。 如果我们只是在写一个命令行应用,并且我们的包不会被当做一个库使用的话,那么我就可以使用devDependencies
。
危险信号
/// <reference path="..." />
不要在声明文件里使用/// <reference path="..." />
。
/// <reference path="../typescript/lib/typescriptServices.d.ts" />
....
应该使用/// <reference types="..." />
代替
/// <reference types="typescript" />
....
务必阅读[使用依赖](./Library Structures.md#consuming-dependencies)一节了解详情。
打包所依赖的声明
如果你的类型声明依赖于另一个包:
- 不要把依赖的包放进你的包里,保持它们在各自的文件里。
- 不要将声明拷贝到你的包里。
- 应该依赖于npm类型声明包,如果依赖包没包含它自己的声明的话。
公布你的声明文件
在发布声明文件包之后,确保在DefinitelyTyped外部包列表里面添加一条引用。 这可以让查找工具知道你的包提供了自己的声明文件。
发布到@types
@types下面的包是从DefinitelyTyped里自动发布的,通过 types-publisher工具。 如果想让你的包发布为@types包,提交一个pull request到https://github.com/DefinitelyTyped/DefinitelyTyped。 在这里查看详细信息contribution guidelines page。
TypeScript 声明文件使用
在TypeScript 2.0,获取、使用和查找声明文件变得十分容易。 这篇文章将详细说明怎么做这三件事。
下载
在TypeScript 2.0以上的版本,获取类型声明文件只需要使用npm。
比如,获取lodash库的声明文件,只需使用下面的命令:
npm install --save @types/lodash
使用
下载完后,就可以直接在TypeScript里使用lodash了。 不论是在模块里还是全局代码里使用。
比如,你已经npm install
安装了类型声明,你可以使用导入:
import * as _ from "lodash";
_.padStart("Hello TypeScript!", 20, " ");
或者如果你没有使用模块,那么你只需使用全局的变量_
。
_.padStart("Hello TypeScript!", 20, " ");
查找
大多数情况下,类型声明包的名字总是与它们在npm
上的包的名字相同,但是有@types/
前缀, 但如果你需要的话,你可以在 TypeScript: Search for typed packages这里查找你喜欢的库。
注意:如果你要找的声明文件不存在,你可以贡献一份,这样就方便了下一位要使用它的人。 查看DefinitelyTyped 贡献指南页了解详情。
更多推荐
所有评论(0)