目前typescript已经非常流行了,我们经常在开发中用到类型、接口、枚举等功能,但一般业务开发中很少需要用到声明文件,只有在诸如扩展一个全局变量、声明一些全局类型的情况下会用到。但如果要写一个node包,那么声明文件就是必不可少的,typescript的声明文件有多种形式,而且不同形式的用法差别很大,本文主要汇总了声明文件的书写方法。
声明文件的定义
声明文件一般包含一些变量和函数的类型定义,如C、C++的.h
头文件。在typescript中以.d.ts
为扩展名的文件是声明文件。
声明文件的查找规则
当在代码中通过import
关键字引入某个路径或库的时候,typescript会以某种规则检测对应的文件是否存在,并引入对应的文件,检测文件存在的解析模式分为传统模块解析和node模块解析,默认使用的是后者。
传统模块解析
以路径名引入:查找对应路径下的.ts
和.d.ts
文件
如在/a/b/c.ts
文件中import depend from '../depend'
,文件查找顺序是:/a/depend.ts; /a/depend.d.ts; [[Error]]
以模块名引入:根据目录名逐层查找对应的.ts
和.d.ts
文件
如在/a/b/c.ts
文件中import depend from 'depend'
,文件查找顺序是:/a/b/depend.ts; /a/b/depend.d.ts; /a/depend.ts; /a/depend.d.ts; /depend.ts; /depend.d.ts; [[Error]]
node模块解析
以路径名引入:查找对应路径下的.ts
和.d.ts
文件 -> 如果引入的路径是一个node包目录,读取node包package.json的types字段对应的文件 -> 读取node包根目录下的index.ts
和index.d.ts
文件
如在/a/b/c.ts
文件中import depend from '../depend'
,文件查找顺序是:/a/depend.ts; /a/depend.d.ts; /a/depend/package.json[[types]]; /a/depend/index.ts; /a/depend/index.d.ts; [[Error]]
可见node模块解析的方式相比传统解析增加了判断node包的逻辑,而且对于node包优先读取的声明文件是package.json中types字段声明的文件,如果没有找到才读取根目录下的index.ts
和index.d.ts
文件,我们可以在开发node包的时候灵活运用这些规则。
以模块名引入:类似上面路径名的引入规则,但是在node_modules目录下查找,而且会回溯到上一级目录查找。
如在/a/b/c.ts
文件中import depend from 'depend'
,文件查找顺序是:
/a/b/node_modules/depend.ts;
/a/b/node_modules/depend.d.ts;
/a/b/node_modules/depend/package.json[[types]];
/a/b/node_modules/depend/index.ts;
/a/b/node_modules/depend/index.d.ts;
/a/node_modules/depend.ts;
/a/node_modules/depend.d.ts;
/a/node_modules/depend/package.json[[types]];
/a/node_modules/depend/index.ts;
/a/node_modules/depend/index.d.ts;
/node_modules/depend.ts;
/node_modules/depend.d.ts;
/node_modules/depend/package.json[[types]];
/node_modules/depend/index.ts;
/node_modules/depend/index.d.ts;
[[Error]]
tsc工具默认使用node模块解析,可以使用tsc --moduleResolution classic
切换到传统模块解析。
写一个声明文件
声明文件大体包含以下几种类型,可以根据自己的需要选择对应的写法:
- 声明全局变量 (导入了一个包含全局变量的库,或者定义全局枚举, interface, type等)
- 扩展全局变量或引入的模块
- 写一个node包的声明文件
声明全局变量
一般使用declare
关键字声明一个全局变量或类型,interface和type不需要添加declare,如:
// 声明变量,一般是const声明,给定值
declare const LIMIT_SIZE = 10;
// 声明一个带属性的全局变量,在ts代码中可能这样用`myBundle.getOrigin()`
declare namespace myBundle {
type origin = 1 | 2 | 3;
function getOrigin(): origin;
}
// 声明一个全局函数
declare function setAnchor (id: number): void;
// 声明一个全局类
declare class Earth {
private weight: number;
private rotate(): void;
}
// 声明一个全局枚举值
declare enum DATES { 'MON', 'TUES', 'WEDS', 'THES', 'FRI', 'SAT', 'SUN' }
// interface和type不需要declare
interface Person {
name: string,
age: number
}
type origin = number | string;
// 声明合并的写法,如JQuery既是一个函数,又是一个namespace
declare function JQuery(selector: string): JQuery
declare namespace JQuery {
ajax(...) => ...
}
值得注意的一点是,全局变量的声明文件不可以带import, export
, 如果需要依赖其他的声明文件,需要使用三斜线语法。
/// <reference types="jquery" />
/// <reference path="../../special.d.ts" />
declare function foo(options: JQuery.Ajax): Promise<any>;
另外需要强调的一点是,声明文件仅仅作为类型声明,不可以用来定义变量,编译后不存在实体,比如声明文件中定义declare enum Platform {...}
,在编译后并不会生成任何js代码,因此希望通过declare
来定义一个在代码工程中处处可用的全局枚举值是不能的,全局变量同理。
扩展全局变量或引入的模块
直接扩充接口:扩充语言类型的底层接口,如:
interface String{
printf(): void;
}
扩充namespace:导入的库中包含全局变量的声明,扩充此声明,如:
// 给JQuery加一个fetch方法的声明
declare namespace jQuery {
fetch(): Promise<unknown>
}
扩充模块:一般在一些插件中会用到,使用declare module + export
语法。
// 插件给dayjs库增加一个foo方法
import * as dayjs from 'dayjs';
declare module 'dayjs' {
export function foo(): dayjs.DayUnit;
}
写一个node包的声明文件
定义一个ES6风格的node包的导出,使用export
关键字
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
或者使用declare + export一次性导出
export {add, subtract};
declare function add(a: number, b: number): number;
declare function subtract(a: number, b: number): number;
完整案例
/a/b/index.d.ts
定义声明文件:
export {add, subtract};
declare function add(a: number, b: number): number;
declare function subtract(a: number, b: number): number;
/a/b/index.ts
功能的具体实现,这里使用了commonjs规范:
function add(a: number, b: number) {
return a + b;
}
function subtract(a: number, b: number) {
return a - b;
}
module.exports = {
add,
subtract
}
/a/b/package.json
定义模块types文件路径,根据上文模块查找规则,这里package.json中定义types可以引导ts编译器查找/a/b
目录下的index.d.ts
文件作为类型声明文件:
{
"types": "./index.d.ts"
}
/a/c/main.ts
引用定义的模块:
import { add } from '../b';
console.log(add(1, 2));
export =
导出格式
export =
是commonjs风格的导出格式,使用这种方式导出的包,导入的时候应该使用import ... = require()
语法,如声明文件为:
export = foo;
declare function foo(): void;
declare namespace foo() {
function bar(): string;
}
上面导出的foo既是一个函数,又可以作为对象取值,如果想要导入foo作为函数使用,就只能通过import foo = require('foo')
这种方式,而不能使用import from
语法。
举例来说,可以将上文案例中/a/b/index.d.ts
的内容变成:
export = numUtil;
declare function numUtil(): void;
declare namespace numUtil {
function add(a: number, b: number): number;
function subtract(a: number, b: number): number;
}
/a/b/index.ts
模块的实现稍加修改为:
// function add ...
// ... 以上是add和subtract的实现
const numUtil = function() {
console.log('numUtil');
}
numUtil.add = add;
numUtil.subtract = subtract;
module.exports = numUtil;
在/a/c/main.ts
中使用该模块:
import numUtil = require('../b');
numUtil();
console.log(numUtil.add(1, 2));
其他
如果node包扩充了底层接口,需要用到declare global
, 如:
// 这个包为String的原型添加了printf方法
declare global {
interface String{
printf(): void;
}
}
如果node包依赖全局变量,但全局变量的声明文件中没有export,可以使用三斜线语法导入:
/// <reference types="node" />
export function foo(p: NodeJS.process): string;
兼容UMD库:UMD同时支持import和script标签引入(一般会在globalThis上绑定一个全局变量),需要额外export一个全局命名空间,如:
export as namespace numUtil;
export {add, subtract};
declare function add(a: number, b: number): number;
declare function subtract(a: number, b: number): number;
语法速查表
语法 | 导入/导出 | 作用 |
---|---|---|
declare const/namespace/class/function/enum | 使用/// 语法导入其他声明文件 | 声明全局变量 |
interface/type | 使用/// 语法导入其他声明文件 | 声明全局类型/接口/枚举 |
declare namespace JQuery | 扩充全局变量 | |
import + declare module | 扩充模块 | |
export / declare + export | 引入格式import … from … | 定义node包 |
export = | 引入格式import … = require(’…’) | 定义node包 |
declare global | node包扩充底层接口 | |
export as namespace | 包兼容UMD库 |