TypeScript 类型声明文件

声明

在 TypeScript 中安全地使用 JavaScript 的库,关键的步骤就是使用 TypeScript 中的一个 declare 关键字。通过使用 declare 关键字,我们可以声明全局的变量、方法、类、对象。

declare 变量

在运行时,前端代码 <script> 标签会引入一个全局的库,再导入全局变量。此时,如果想安全地使用全局变量,那么就需要对变量的类型进行声明。

声明变量的语法: declare (var|let|const) 变量名称:变量类型

declare let a: string
declare const b: number
a = 'a'
a = 1 // 报错
b = 2 // 报错,const 声明不能再赋值

declare 函数

声明函数的语法与声明变量的语法相同,不同的是 declare 关键字后需要跟 function 关键字。

/* 报错,只需声明,不需要实现
declare function toString(x: number) {
  return String
} */
// 正确的语法
declare function toString(x: number): string
const x = toString(1)

declare 类

声明类时,也是只需要声明类的属性、方法的类型即可。

注意:使用 declare 关键字时,我们不需要编写声明的变量、函数、类的具体实现(因为变量、函数、类在其他库中已经实现了),只需要声明其类型即可,否则会报错。

declare 枚举

声明枚举只需要定义枚举的类型,并不需要定义枚举的值,这其实就是枚举中的外部枚举(Ambient Enums)

注意:声明枚举仅用于编译时的检查,编译完成后,声明文件中的内容在编译结果中会被删除。

例如上面的代码,编译完成后,相当于仅剩下了 const direction = [Direction.Up, Direction.Down],这里数组中的 Direction 表示引入的全局变量。

declare 模块

ES6 之前,TypeScript 提供了通过使用 module 关键字声明一个内部模块的模块化方案,但是由于 ES6 也使用了 module 关键字,为了兼容 ES6,所以 TypeScript 使用 namespace 替代了原来的 module,并更名为命名空间。需要注意:目前,任何使用 module 关键字声明一个内部模块的地方,都应该使用 namespace 关键字进行替换。

TypeScript 与 ES6 一样,任何包含顶级 import 或 export 的文件都会被当作一个模块。我们可以通过声明模块类型,为缺少 TypeScript 类型定义的三方库或者文件补齐类型定义。

声明模块的语法:declare module '模块名' {},在模块声明内部,只需要使用 export 导出对应库的类、函数即可。

declare 文件

在使用 TypeScript 开发前端应用时,可以通过 import 关键字导入文件,比如先使用 import 导入图片文件,再通过 webpack 等工具处理导入的文件。但是,因为 TypeScript 并不知道通过 import 导入的文件是什么类型,所以需要使用 declare 声明导入的文件类型。

上面的代码标记的图片文件的默认导出类型是 string,通过 import 使用图片资源时,TypeScript 会将导入的图片识别为 string 类型,因此也就可以把 import 的图片赋值给 src 属性,因为它们的类型都是 string,是匹配的。

declare namespace

不同于声明模块,命名空间一般用来表示具有很多子属性或者方法的全局对象变量。

一般我们可以将声明命名空间简单看作是声明一个更复杂的变量。

在上面的例子中,因为我们声明了全局导入的 jQuery 变量 $,所以可以直接使用 $ 变量的 version 属性以及 ajax 方法。

声明文件

在 TypeScript 中,以 .d.ts 为后缀的文件为声明文件。在声明文件时,我们只需要定义第三方类库所暴露的 API 接口即可。

声明文件中有类型、值、命名空间 3 个核心概念:

  • 类型(以下每一个声明都创建了一个类型名称)

    • 类型别名声明;

    • 接口声明;

    • 类声明;

    • 枚举声明;

    • 导入的类型声明

  • 值(值就是在运行时表达式可以赋予的值)

    • var、let、const 声明;

    • namespace、module 包含值的声明;

    • 枚举声明;

    • 类声明;

    • 导入的值;

    • 函数声明

  • 命名空间 在命名空间中,也可以声明类型。比如 const x: A.B.C 这个声明,这里的类型 C 就是在 A.B 命名空间下的。

使用声明文件

安装 TypeScript 依赖后,一般我们会顺带安装一个 lib.d.ts 声明文件,这个文件包含了 JavaScript 运行时以及 DOM 中各种全局变量的声明。lib.d.ts 文件内容如下:

其中,/// 是 TypeScript 中三斜线指令,后面的内容类似于 XML 标签的语法,用来指代引用其他的声明文件。

通过三斜线指令,可以更好地复用和拆分类型声明。no-default-lib="true" 表示这个文件是一个默认库。而最后 4 行的 lib="..." 表示引用内部的库类型声明。

使用 @types

Definitely Typedarrow-up-right 是最流行的高质量 TypeScript 声明文件类库,正是因为有社区维护的这个声明文件类库,大大简化了 JavaScript 项目迁移 TypeScript 的难度。

目前,社区已经记录了 90% 的 JavaScript 库的类型声明,意味着如果我们想使用的库有社区维护的类型声明,那么就可以通过安装类型声明文件直接使用 JavaScript 编写的库了。

具体操作:首先,通过此链接arrow-up-right搜索你想要导入的库的类型声明,如果有社区维护的声明文件。然后,只需要安装 @types/xxx 就可以在 TypeScript 中直接使用它了。

类型合并

因为 Definitely Typed 是由社区人员维护的,如果原来的第三方库升级,那么 Definitely Typed 所导出的第三方库的类型定义想要升级还需要经过 PR、发布的流程,就会导致无法与原库保持完全同步。针对这个问题,在 TypeScript 中,可以通过类型合并、扩充类型定义的技巧临时解决。

合并接口

最简单、常见的声明合并是接口合并,需要注意的是接口的非函数成员类型必须完全一样

对于函数成员而言,每个同名的函数声明都会被当做这个函数的重载。需要注意的是:接口内部的函数声明优先级按照顺序确定,接口之间的函数声明后面声明的接口具有更高的优先级,如果函数声明指定的参数是字面量类型,优先级最高

合并 namespace

合并 namespace 与合并接口类似,命名空间的合并也会合并其导出成员的属性,需要注意的是导出的成员是不能重复的。另外不同的是,非导出成员仅在原命名空间内可见。

在上面的例子,同名的命名空间 Person 中,有一个非导出的属性 age,在第二个命名空间 Person 中没有 age 属性却引用了 age,所以 TypeScript 报错找不到 age。

合并 namespace 和 函数

同名的 namespace 和 函数 合并,命名空间中导出的成员相当于给 函数 添加属性。

类不可合并

定义一个类类型,相当于定义了一个类,又定义了一个类的类型。因此,对于类这个既是值又是类型的特殊对象不能合并。

扩充模块

除了可以通过接口和命名空间合并的方式扩展原来声明的类型外,还可以通过扩展模块或扩展全局对象来增强类型系统。

JavaScript 是一门动态类型的语言,通过 prototype 可以很容易地扩展原来的对象。

但是,如果直接扩展导入对象的原型链属性,TypeScript 会提示没有该属性的错误,所以还需要扩展原模块的属性。

类似上面的代码,对于导入的第三方模块,同样可以使用这个方法扩充原模块的属性。

扩充全局

全局模块指的是不需要通过 import 导入即可使用的模块,如全局的 window、document 等。

推荐阅读

Last updated