当使用扩展的JavaScript库或者插件API的时候,将需要使用声明文件(.d.ts)来描述库的类型。本文内容将包括如何编写声明文件相关的一些高级概念,然后用一些例子来展示如何将各式各样的概念与声明文件的描述相匹配。
流程
写.d.ts最好是从库的说明文档开始,而不是代码。从说明文档开始可以保证思维不受实现细节的影响,并且比阅读JS代码容易理解。下面的例子假设是根据说明文档写的,并且提供调用代码。
命名空间
当定义接口(例如"options"对象)的时候,你可以选择是否将这些类型放入一个模块中。这需要主观判断,如果.d.ts文件使用者更多的是用这些类型定义变量或者参数,并且类型的命名不会产生冲突,则将其放在全局命名空间较好。如果该类型不能够被直接引用,或者不能给一个唯一并且合理的命名,使用一个模块来防止与其他类型的冲突。
回调函数
很多JavaScript库都将函数作为参数,之后传入调用此函数时所用到的已知参数列表。在编写这些类型的函数签名时,不可以将这些参数标记为可选参数。正确的方式是想想"需要提供哪些参数?"(针对使用.d.ts的开发人员),而不是"哪些参数将被用到?"(针对函数被调用的时候)。
可扩展性和声明合并
当编写定义文件时,需要记住TypeScript的扩展现有对象的规则。可以选择使用匿名函数类型或者接口类型来声明一个变量:
声明一个匿名的类型:
declare var MyPoint: { x: number; y: number; };
声明一个接口类型:
interface SomePoint { x: number; y: number; }
declare var MyPoint: SomePoint;
从使用者角度来看,这些声明其实是一样的,但是SomePoint类型能够通过接口合并扩展:
interface SomePoint { z: number; }
MyPoint.z = 4; // OK
是否想让声明的变量可扩展是主观判断的。通常这也比较符合JavaScript库的目的。
类的分解
TypeScript中,类会创建两种单独的类型:实例类型,定义类的实例有哪些成员类型;构造函数类型,定义类的构造函数有哪些成员类型。构造函数的类型也被称为“静态部分”类型,因为它包含了类的静态成员。
虽然你可以使用关键字"typeof"来获取类的静态部分的类型,有时候使用类的分解模式来写定义文件是很有必要的,它可以明确的分离类的实例和静态类型。
标准模式:
class A {
static st: string;
inst: number;
constructor(m: any) {}
}
分解模式:
interface A_Static {
new(m: any): A_Instance;
st: string;
}
interface A_Instance {
inst: number;
}
declare var A: A_Static;
两种模式差异:
1.标准模式的类可以使用extends继承;分解模式不可以。这可能在TypeScript以后的版本中被改善,如果可以需要允许任意使用extends表达式。
2.都允许在后面添加静态部分的成员(通过合并声明)。
3.分解模式允许在后面添加实例部分的成员,而标准模式的不允许。
4.当使用分解模式的时候,需要为更多的类起一个合理的名称。
命名规则
一般来说,不需要给接口加上前缀I(如:IColor)。因为TypeScript中的接口比C#或Java里的接口具有更广泛的意义,加I的命名规则基本上没什么用。
案例
下面看例子吧。每个例子都已经提供了库的简单使用(这里需要自己对函数/对象进行脑补),然后就是定义精准类型的代码。如果有多个良好的声明方式,也会列出来。
选项对象(参数选项)
使用代码:
animalFactory.create("dog"); // 未通过验证: 如果给定options,必须提供name
animalFactory.create("giraffe", { name: "ronald" });
animalFactory.create("panda", { name: "bob", height: 400 });
animalFactory.create("cat", { height: 32 }); // 未通过验证: 如果给定options,必须提供name
类型声明:
module animalFactory {
interface AnimalOptions {
name: string;
height?: number;
weight?: number;
}
function create(name: string, animalOptions?: AnimalOptions): Animal;
}
带有属性的函数
使用代码:
zooKeeper.workSchedule = "morning";
zooKeeper(giraffeCage);
类型声明:
// 注意:函数必须处于模块之前
function zooKeeper(cage: AnimalCage);
module zooKeeper {
var workSchedule: string;
}
可使用关键字new也可直接调用的方法
使用代码:
var w = widget(32, 16);
var y = new widget("sprocket");
// w和y都是widgets
w.sprock();
y.sprock();
类型声明:
interface Widget {
sprock(): void;
} interface WidgetFactory {
new(name: string): Widget;
(width: number, height: number): Widget;
} declare var widget: WidgetFactory;
全局/封闭的库
使用代码:
// 可以这样写
import x = require('zoo');
x.open();
// 或者
zoo.open();
类型声明:
module zoo {
function open(): void;
} declare module "zoo" {
export = zoo;
}
外部模块中的单一复杂对象
// 可链式操作的eagles
import eagle = require('./eagle');
// 直接调用
eagle('bald').fly();
// 使用关键字"new"
var eddie = new eagle(1000);
// 设置属性
eagle.favorite = 'golden';
类型声明:
// 注意:在这里可以使用任何名称,但是整个文件中名称都要相同。
declare function eagle(name: string): eagle;
declare module eagle {
var favorite: string;
function fly(): void;
}
interface eagle {
new(awesomeness: number): eagle;
} export = eagle;
// 顺带加一点,node编译需要加--module,这个在TypeScript Modules(模块)中有提到,也给出了可运行的案例
回调函数
使用代码:
addLater(3, 4, (x) => console.log('x = ' + x));
类型声明:
// 注意:"void"返回类型在这里优先
function addLater(x: number, y: number, (sum: number) => void): void;
本篇基本脑补过来的... 后续如果工作中有使用到再记录详细的使用,之前一些列的TypeScript使用手册的随笔中,代码是经过修改成可运行的,还需要继续努力,不断改善自己,编写更高质量的代码。