声明文件就是用typescript 的类型语言,描述库对外提供的变量,函数,模块的类型。javascript 是没有类型的,很多已经存在的库,都是用javascript 开发,在使用typescript 引用这类库的时候,那如何知道对应的类型,这就要借助声明文件。它主要有以下3个主要的关键点:
-
声明文件格式:
[filename].d.ts
. -
它就实际上就是一个中间人的角色,为javascript 提供类型说明
-
有的项目是直接使用typescript 开发,有时候也会使用声明文件去规划类型的设计, 把类型的声明和业务逻辑分离
在设计类型时,只关注类型设计,完全是一种声明写法,使得typescript 拥有绝对抽象能力,不再关心实现的细节,对团队的协作,帮助巨大。
不管是为javascript 提供类型说明,还是作为类型设计,本质上一样的,只是使用目的上的区别。
声明参考
一些经常使用的数据格式(如:对象方法/属性、函数),我们看看他们的声明文件写法。
对象的属性方法
// 一个对象上对外提供方法/属性
export const obj = {
getGreeting(str) {
return `hello ${str}`;
},
count: 10,
};
/**
* 声明写法
* 使用declare namespace描述由点号访问类型或值。
*/
declare namespace myLib {
function getGreeting(srt: string): string;
let count: number;
}
函数重载
function getTypeList(input) {
if (typeof input === "string") {
return `hello world of ${input}`;
} else if (typeof input === "number") {
return [input];
}
}
/**
* 声明写法
* 函数重载:函数名一样,参数不一样,返回的类型不一样
*/
declare function getTypeList(str: string): string;
declare function getTypeList(num: number): number[];
组织类型
当一个类库比较大的时候,就要考虑拆分根据职责,细分成不同的命名空间,不同的接口;这样降低复杂度,方便维护。
// 使用命名空间来组织类型,拆分为不同的接口
/**
* 声明写法
* 商品分类,我们列举其中2类,在同一个命名空间,用不的的接口去声明
*/
declare namespace Production {
// 计算机
interface Computer {
// 内核数
core: number;
// 尺寸
size: string;
}
// 水果
interface Apple {
// 单价
price: number;
}
}
// 也是支持嵌套命名空间, 通过圆点访问
declare namespace Production.Fruit {
interface Banana {}
interface Orange {}
}
类的声明
// 业务逻辑上的类
class Greeting {
word = "";
constructor(str: string) {
this.word = str;
}
echo(str: string) {
console.log(str || this.word);
}
}
/**
* 声明写法
* 构造函数,方法,属性的声明
*/
declare class Greeting {
constructor(str: string);
word: string;
echo(str: string): void;
}
全局变量
var globalVar = 'hello world'
/**
* 声明写法
*/
declare var globalVar: string;
全局函数
// 在全局作用域上
var greet = (str) => console.log(str);=
/**
* 声明写法
*/
declare function greet(greeting: string): void;
全局的直接全用
declare
声明,局部的使用declare namespace
命名空间,防止冲突。可以根据实际情况使用。
库结构分析
在为一个 JavaScript 库写声明文件时,首先要确定这个库的类型,是commonjs,es 模块,还是全局的类库,UMD Y库,他们都有一些主要特征,我们可以从下代码层面去分析:
- 没有明确引用,在任何地方直接使用require 或者 define
- 使用 import * as a from 'b'; or export c 声明引用、导出;
- 给 exports or module.exports 赋值
- 直接 window or global 对象赋值、
识别模块化的库
主要是一个函数,一个class类,commonjs,es 的模块, 文件档是都给出对应的模板。参数前面的声明参考,在理解的基础上去写,是比较简单的。
- CommonJS/Node.js 样式的表单导入 var fs = require("fs");
- 描述如何require或导入库的文档
- 给 exports or module.exports 赋值
- 使用 import * as a from 'b'; or export c 声明引用、导出;
识见全局库
全局库可以从调用上,声明的关键字上去找到一些线索:
- 顶级var语句或function声明
- 调用时 window.someName, 在任何地方直接调用
- DOM 原语喜欢document或window存在的假设
识别全局库 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) {
声明文件依赖
一个声明文件依赖其它的声明文件,声明文件会因类库不一样,引入的方式也会不一样
对全局库的依赖
如果您的库依赖于全局库,请使用/// 指令
/// <reference types="someLib" />
function getThing(): someLib.thing;
对模块的依赖
如果您的库依赖于某个标准es模块,请使用以下import语句:
import * as moment from "moment";
function getThing(): moment;
对 UMD 库的依赖
/// <reference types="moment" />
function getThing(): moment;
ES6 对模块调用签名的影响
//命名空间导入类似import * as moment from "moment"的作用一样const moment = require("moment")
// ES6 模块规范规定命名空间 import ( import * as x) 只能是一个对象, 不可以执行
import exp = require("express");
var app = exp();
在符合 ES6 的模块加载器中,顶级对象(这里导入为exp)只能有属性;顶级模块对象永远无法调用。
打开esModuleInterop将解决 TypeScript 转译的代码中的这两个问题。
- 第一个改变了编译器的行为,
- 第二个是通过两个新的辅助函数修复的,它们提供了一个 shim 来确保发出的 JavaScript 的兼容性:
模板的链接
抽象能力
在一个新需求澄清后,通常是要对业务进行抽象,一些需求细节还没有明确,一些依赖后台的接口字段还在讨论中,但是开发已经启动了,这个时候,typescipte 抽象能力,就排上用场了,可以忽略上面提到的细节。设计业务模块,抽象类,抽象参数
interface LoginResponse {
userId: number;
name: string;
}
interface LoginParams {
name: string;
pwd: string;
}
abstract class AbstractLogin {
// 登录
abstract login(params: LoginParams): Promise<LoginResponse>;
// 登出
abstract logout(): Promise<boolean>;
}
/**
* 侧重的是抽象, 细节可以放在后面实现
* 这是一个很重要的能力,可以先聚焦一点,其它的可以延后处理
*/
export class LoginUser implements AbstractLogin {
login(params: LoginParams) {
// do something, 不作细节处理了,实际会调用后台登录接口
console.log(params);
return Promise.resolve({ userId: 1, name: "hello world" });
}
logout() {
// 实现的细节:如清空token,调用到后台接口注销,权限注销
// 我们都可以搁置,可以放在后面提供,也可以由其它人来实现
// 这样思路比较明确,降低代码维护成本
return Promise.resolve(true);
}
}
结语
在决定要为库写声明文件时,首先是要根据特征识别出是什么库,然后找到对应的模块参考一下。当然你如果对声明的方式,了然于胸,直接组织声明更好。实际上很多第三库都提供有声明文件,现在写声明文件更多的是用在类型设计上,为开发者提供业务抽象能力,赶紧试试吧。
文章中如有错漏,欢迎指正,谢谢。