TypeScript 声明

138 阅读8分钟

前言

声明文件是将一些全局变量或者引入的模块对应的类型声明语句存在一个单独的文件中,文件后缀一定要为 d.ts ,这样的文件就被成为声明文件。通过这种方式有助于定义在项目中或第三方库中使用的类型,这样可以使得 TypeScript 在编译时能够检查出类型错误。

什么是声明文件

声明文件的定义

将一些全局变量或者引入的模块对应的类型声明语句存在一个单独的文件中,文件后缀一定要为 d.ts ,这样的文件就被成为声明文件。

例如项目中定义了一个 jquery.d.ts 的声明文件

declare var jQuery: (selector: string) => any;

那么之后项目中所有的ts文件都可以正常使用 jQuery, 而不会出现ts报错

jQuery('#root')

注意的是, 声明文件必须是在 ts.config.json 文件 include 属性定义的范围内才会生效. 这个涉及到tsc的编译范围.

Ts模块解析

是指编译器在查找导入模块内容时所遵循的流程, 编译器需要准确知道它表示什么, 并且需要检查它的定义.

编译在尝试定位导入模块文件的时候,会遵循两种策略: ClassicNode , 可以是用 --moduleResolutio 来标记是使用那种模块解析策略. 那么在使用了 --module AMD | System | ES2015时的默认值为 Classic , 其它情况时则为 Node .

  • classic 存在的理由是为了向后兼容
  • Node 模块则是为了新的标准

相对模块的加载

相对导入是以 / ./ ../ 开头的. 其他形式导入都是非相对的

// 假设当前执行路径为 /root/src/modulea
import { b } from './moduleb'
  • 首先寻找 /root/src/moduleb.ts 是否存在,如果存在使用该文件。
  • 其次寻找 /root/src/moduleb.tsx 是否存在,如果存在使用该文件。
  • 其次寻找 /root/src/moduleb.d.ts 是否存在,如果存在使用该文件。
  • 其次寻找 /root/src/moduleB/package.json,如果 package.json 中指定了一个types属性的话那么会返回该文件。
  • 如果上述仍然没有找到,之后会查找 /root/src/moduleB/index.ts
  • 如果上述仍然没有找到,之后会查找 /root/src/moduleB/index.tsx
  • 如果上述仍然没有找到,之后会查找 /root/src/moduleB/index.d.ts

非相对导入

// 假设当前文件所在路径为 /root/src/modulea
import { b } from 'moduleb'
  • /root/src/node_modules/moduleB.ts
  • /root/src/node_modules/moduleB.tsx
  • /root/src/node_modules/moduleB.d.ts
  • /root/src/node_modules/moduleB/package.json(如果它指定了一个types属性)
  • /root/src/node_modules/@types/moduleB.d.ts
  • /root/src/node_modules/moduleB/index.ts
  • /root/src/node_modules/moduleB/index.tsx
  • /root/src/node_modules/moduleB/index.d.ts

模块加载, 文件名称不区分大小写, 因此定义moduleB 文件, 也能正常查找到模块.

TypeScript常见的库类型

UMD库

通用模块定义的方式,能够在任何地方执行,无论是客户端还是服务端。UMD库通常会在全局变量中暴露一个名为libraryName的变量,它可以作为一个对象来调用库中的方法。

既可以通过 script 标签引入, 也可以通过 import 导入

ES6模块库

ES6模块库是使用ES6模块系统编写的库。它们通常使用import语句来导入库中的代码,并使用export关键字来将代码暴露给其他模块使用。

ES6模块库的特点如下:

  • 与CommonJS和AMD模块不同,ES6模块是静态的,这意味着它们在编译时解析并且不支持动态导入。这个特点使得编译器能够在编译时执行优化,从而提高性能。
  • ES6模块使用importexport关键字来导入和导出模块中的代码。
  • ES6模块支持命名导入和默认导入。命名导入允许开发者将模块中的指定部分导入到当前模块中。默认导入允许开发者导入模块的默认输出。
  • 与CommonJS和AMD模块不同,ES6模块可以从任何文件系统路径中导入模块,而不仅仅是从顶层目录或当前目录中导入。

使用ES6模块库的优点包括:

  • ES6模块的静态特性使得编译器可以对代码进行优化,从而提高性能。
  • ES6模块使用importexport关键字来导入和导出代码,这使得代码更加清晰易懂。
  • ES6模块允许开发者从任何文件系统路径中导入代码,这使得代码更加灵活。

声明文件库

声明文件库是使用TypeScript编写的库,这些库通常需要单独的.d.ts声明文件。这些库可以是UMD库或ES6模块库,但它们通常使用TypeScript类型来提供类型安全。

声明文件中会定义该库中的函数、类、变量、接口等等,以及它们的类型信息让 IDE 或者编辑器给出更好的代码提示和错误提示,从而提高代码的可维护性和可读性。

声明文件库可以是UMD库或ES6模块库,但它们通常使用TypeScript类来提供类型安全。在使用声明文件库时,只需要在项目中引入该库的, 声明文件即可,就可以获得该库中所有的类型信息。声明文件通常以.d.ts为后缀,可以手动编写,也可以使用第三方工具自动生成。

如果你在使用一个没有提供类型声明文件的第三方库,可以自己手动编写一个声明文件,或者使用一些第三方工具自动生成。在编写声明文件时,可以使用declareinterfacenamespacetype等关键字来定义类型信息。

全局变量库

全局变量库是指没有模块系统,而是在全局作用域中定义一个或多个变量的库。这些库通常是使用JavaScript编写的,但是可以使用TypeScript的声明文件为其提供类型安全。

全局变量库通常包含各种全局变量、函数、类等,这些变量可以在任何地方访问,但是如果没有进行适当的命名约定,它们很容易与其他库中的变量发生冲突。因此,使用全局变量库时需要采用一些良好的编程实践,例如使用唯一的变量名、在变量名中包含库名称、使用命名空间等。

同时,为全局变量库编写声明文件可以提供类型安全,让开发者在使用全局变量库时获得更好的代码提示和错误检查功能。

声明文件编写

声明文件的编写方式有以下几种

  • 使用declare关键字:用于声明一个全局变量的类型,可以包含函数、类、变量等内容。例如:
declare var jQuery: (selector: string) => any;

这个声明告诉TypeScript编译器,存在一个名为jQuery的全局变量,它是一个函数,接收一个字符串类型的参数,返回任意类型的值。

  • 使用interface关键字:用于声明一个接口类型,可以包含属性、方法等内容,用于描述一个对象的形状。例如:
interface Person { name: string; age: number; }

,这个声明表示一个Person接口,包含nameage两个属性,分别是字符串和数字类型。

  • 使用namespace关键字:用于声明一个命名空间,可以包含函数、类、变量等内容。通过命名空间,可以避免全局变量冲突的问题。例如:
namespace MyNamespace { export const PI = 3.14; }

这个声明表示一个名为MyNamespace的命名空间,包含一个常量PI,值为3.14,并通过export关键字将PI导出。

  • 使用type关键字:用于声明一个类型别名,可以为一个类型取一个新的名字。例如:
type MyType = string | number

这个声明表示一个名为MyType的类型别名,表示可以是字符串或者数字类型。

扩展全局变量

要扩展全局变量,可以使用declare global语句。例如,如果要扩展全局Array类,可以在声明文件中添加以下代码:

declare global {
  interface Array<T> {
    shuffle(): T[];
  }
}

Array.prototype.shuffle = function() {
  // 省略实现代码
}

这个声明表示在全局范围内扩展Array类,添加一个shuffle方法,该方法可以用于数组元素的随机排序。在代码中,就可以像下面这样使用shuffle方法:

const arr = [1, 2, 3];
arr.shuffle();

扩展Npm包、UMD的全局变量

任何声明文件中只要存在 export/import 关键字的话,该声明文件中的 declare 都会变成模块内的声明而非全局声明。

// types/axios.d.ts
declare function axios(): string;

// 此时声明的 interface 为模块内部的String声明
declare interface String {
  hello: () => void;
}
export default axios;

// index.ts
'a'.hello() // 类型“"a"”上不存在属性“hello”

针对Npm包中需要全局声明的时候, TS 为我们提供了 decalare global 来解决问题

// types/axios.d.ts
declare function axios(): string;

// 模块内部通过 declare global 进行全局声明
// declare global 内部的声明语句相当于在全局进行声明
declare global {
  interface String {
    hello: () => void;
  }
}
export default axios;

// index.ts
'a'.hello() // correct

扩展Npm包类型

利用 declare module 的语法来进行模块声明的同时, 也可以使用它来对于已有第三方库进行类型定义文件的扩展。

需要额外注意, 如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module 扩展原有模块.

例如: 扩展 第三方库axios 的是 请求参数配置定义 AxiosRequestConfig

import axios, { AxiosRequestConfig } from 'axios';
declare module 'axios' {
    interface AxiosRequestConfig {
        cancelRepeatRequest?: boolean | string; // 取消请求配置
    }
}

此时, 代码中无论我们是取值还是赋值,都会有对应的TS类型提示.

参考资料