使用声明文件

TypeScript 声明文件的作用是描述 JavaScript 模块内各个声明的类型信息,IDE 或编译器在获得这些信息后,就提供代码补全、接口提示、类型检查等功能。

声明文件以 .d.ts 为扩展名,它不包含任何具体实现代码(如函数体或变量具体值),仅包含类型声明,接下来进行详细介绍。

使用声明文件的原因

通常来说,项目中的历史代码或第三方库都是用 JavaScript 编写的,由于 JavaScript 本身并不包含类型信息,因此直接将历史代码或第三方库引入 TypeScript 项目中可能引起编译错误。

假设当前项目结构如下。

D:\TSProject
    library.js
    index.ts
    tsconfig.json

library.js 是一段历史代码,它用 JavaScript 编写,包含一个公共函数 sum(),用于计算两数之和,其代码如下。

export function sum(a, b) {
    return a + b;
}

index.ts 文件如下,它引用了 library.js 文件,然后输出 1 和 2 相加的结果。

import { sum } from './library.js'
console.log(sum(1, 2));

tsconfig.json 文件如下,它使用了严格模式。关于严格模式的更多信息,请参考18.1.5节。

{
  "compilerOptions": {
    "strict": true,
    "outDir": "dist"
  }
}

由于使用了严格模式,TypeScript 编译器要求每个变量、方法都必须有明确的类型,不允许各个声明隐式具有 any 类型,因此 index.ts 文件的第一行代码将引起编译错误,如图21-1所示。

image 2024 02 20 12 03 16 416
Figure 1. 图21-1 编译错误

要快速解决这个问题,有几种办法,例如,编辑 tsconfig.json 文件,去掉 "strict":true,但这样所有文件(包括 TypeScript 文件)都无法使用严格模式的检查,这显然不是我们需要的。

另一种办法是单独为 JavaScript 忽略各项编译检查,例如,在 tsconfig.json 文件中增加编译选项 "allowJs": true,代码如下。

{
  "compilerOptions": {
    "strict": true,
    "outDir": "dist",
    "allowJs": true
  }
}

该编译选项将告知编译器将 JavaScript 文件中的所有声明都当作 any 类型而不做其他要求。

但这只最低限度地保证 JavaScript 文件能够正常运行。因为其声明全是 any 类型,无法使用代码补全、接口提示、类型检查等功能,所以用这些声明进行开发不仅效率低,而且容易出错。这些错误通常只能在程序运行后才能发现,排错成本较大。如果要描述 JavaScript 中的各个声明真实的类型,就需要定义声明文件。例如,编写 library.d.ts 文件,描述 sum() 函数的各个参数的类型及返回值类型。library.d.ts 的代码如下。

declare function sum(a: number, b: number): number
export { sum }

在这之后,index.ts 文件将不再出现编译错误,并且也可以看到在调用 sum() 函数时,IDE 能够识别出 sum() 函数的参数类型及返回值类型。图21-2所示为 IDE 智能提示。

image 2024 02 20 12 06 24 312
Figure 2. 图21-2 IDE智能提示

为JavaScript编写声明文件

声明文件以 .d.ts 为扩展名,它不包含任何具体实现代码(如函数体或变量具体值),仅包含类型声明。它通过 declare 关键字声明 JavaScript 中需要指明类型的对象。

声明形式通常有以下几种。

  • declare var/let/const:声明全局变量。

  • declare function:声明全局函数。

  • declare class:声明全局类。

  • declare enum:声明全局枚举类型。

  • declare namespace:声明(含有子属性的)对象。

  • declare module:声明模块。

声明模块将在21.2.3节中单独介绍,其他声明形式将在本节中详细介绍。

声明全局变量和全局方法

假设 lib.js 文件的内容如下。其中,声明并导出了一个名为 pi 的常量,然后声明并导出了一个计算圆形面积的函数 calculatingArea(),它需要一个参数 r(圆形半径),返回圆形面积(计算公式为半径的平方乘以圆周率)。

export const pi = 3.14159;
export function calculatingArea(r) {
    return r * r * pi;
}

阅读上述 JavaScript 代码,你可以发现 pir 都是数值类型。接下来,根据这些信息,编写对应的声明文件 lib.d.ts,文件内容如下。

declare const pi: number;
declare function calculatingArea(r: number): number;
export { pi, calculatingArea }

声明全局枚举及函数

假设 lib.js 文件的内容如下。其中,声明了一个名为 printDirection() 的函数,用于输出方向信息,它要求传入一个参数 dirt(方向),然后根据传入的值,输出不同的方向文本。

export function printDirection(dirt) {
    switch (dirt) {
        case 0:
            console.log("up");
            break;
        case 1:
            console.log("down");
            break;
        case 2:
            console.log("left");
            break;
        case 3:
            console.log("right");
            break;
    }
}

阅读上述 JavaScript 代码,你可以发现 printDirection() 函数的参数 dirt 实际上是一个枚举类型。接下来,根据这些信息,编写对应的声明文件 lib.d.ts,声明一个表示方向的常量枚举 directionprintDirection() 函数,函数的参数为 direction 类型。文件内容如下。

export declare const enum direction {
    up = 0,
    down = 1,
    left = 2,
    right = 3
}
export declare function printDirection(dirt: direction): void;

使用接口或类型别名

在类型声明文件中直接使用 interfacetype 来声明一个全局的接口或类型。假设 lib.js 文件的内容如下。其中,声明了一个 printPoint() 函数用于输出坐标,它要求传入一个参数 point(表示坐标),然后在函数体中输出 x 值和 y 值。

export function printPoint(point) {
    console.log(`location is ${point.x},${point.y}.`);
}

阅读上述 JavaScript 代码,你可以发现 point 参数实际上具有一定的结构要求,因此可以用 interfacetype 来声明该结构。接下来,根据这些信息,编写对应的声明文件 lib.d.ts,文件内容如下。

interface Point { x: number; y: number; }
//type Point = { x: number; y: number; } //使用类型别名的方式声明
export declare function printPoint(point: Point): void;

声明全局类

假设 lib.js 文件的内容如下。

export class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    get name() {
        return `${this.firstName} ${this.lastName}`;
    }
    selfIntroduction() {
        console.log(`My name is ${this.name}`);
    }
}

其中声明了一个名为 Person 的类,它不仅具有一个构造函数(分别传入 firstNamelastName),还具有一个存取器 name(用来返回完整姓名),以及一个名为 selfIntroduction() 的函数(用于输出自我介绍文本)。

阅读上述代码,你可以发现类的各个属性及存取器均为 string 类型,而 selfIntroduction() 函数没有返回值,因此为 void 类型。接下来,编写对应的声明文件 lib.d.ts,文件内容如下。

export declare class Person {
    get name(): string;
    firstName: string;
    lastName: string;
    constructor(firstName: string, lastName: string);
    selfIntroduction(): void;
}

声明(含子属性的)对象

此声明方式通常用于闭包。假设 lib.js 文件的内容如下。

export var excelHelper;
(function (excelHelper) {
    excelHelper.fileName = "D:\\x.xls";
    function readExcelCell(row, col) {
        //...
        return cellValue;
    }
    excelHelper.readExcelCell = readExcelCell;
})(excelHelper || (excelHelper = {}));

它声明并导出了一个对象 excelHelper,用于读取 excel 信息,然后声明了一个闭包,并将闭包中局部变量和函数的值通过属性和方法的形式赋给 excelHelper 对象。

接下来,编写对应的声明文件 lib.d.ts。文件内容如下。

export declare namespace excelHelper {
    let fileName: string;
    function readExcelCell(row: number, col: number): string;
}

为TypeScript生成声明文件

TypeScript 文件最终被编译为 JavaScript 文件才能执行。如果这些库文件需要供其他团队使用,相对于同时提供该库的 TypeScript 源文件和 JavaScript 文件,只提供 TypeScript 声明文件及 JavaScript 文件更具优势,因为使用后一种方式不仅文件占用的空间小,而且无须再编译代码。

TypeScript 编译器支持编译时自动生成 .d.ts 文件,只需要在 tsconfig.json 配置文件中开启 declaration 编译选项即可。然后,在使用 tsc 命令编译时,不仅会输出 JavaScript 文件,还会输出目录生成的同名 .d.ts 文件。

{
    "compilerOptions": {
      "declaration": true
    }
}