概述
在编写 Javascript 时,我们会用到 ESM、CommonJs、UMD 等模块系统。Typescript 支持多种模块系统,推荐的是 ESM 和 CommonJs。
与 ES2015 标准相同,Typescript 认为使用了import/export或module语句的 ts 文件为模块,其中的变量的作用域限于文件内,外部想要使用必须先引入相应文件。没有使用import/export或module的文件为脚本,其变量的作用域为全局。
如果一个模块文件只需要运行代码,没有需要导出的变量,可以添加一个:
export {}
这样编译器会认为这是一个模块,不会有全局副作用。
namespace
Typescript 有自己的模块系统——namespace,与 ESM 的设计逻辑有很大不同。随着 ESM 的普及和发展,ESM 的语法更被推荐。
更多 namespace 详情可参考 Typescript 文档:namespace和modules。
类型声明文件
在一些情况下,我们引入的文件(比如 js 文件)没有变量类型声明,编译器无法进行类型检查,这个时候需要类型声明文件(d.ts)来辅助,它声明了一个模块导出的变量及其类型。一个类型声明文件就是声明了变量和函数类型的文件,它的变量和函数命名与 js 文件的导出内容相同,但是不需要实现。
类型声明文件分为两种:模块声明文件和全局声明文件。模块声明文件是写有import/export的类型声明文件,需要和被声明的 js 文件同名且处于同一目录下,或是安装在 node_modules/@types 目录下。全局声明文件即不包含import/export的类型声明文件,处于工作目录(除 node_modules)中即可,编译器会自动遍历并使之生效。
Javascript 模块
ts 文件可以引入 js 文件作为模块,但是由于 js 文件没有类型声明,在严格模式下("strict": true)需要一个模块类型声明文件来显式声明 js 模块成员的类型。
比如有这样的目录结构:
|-- src
| |-- a
| | `-- index.ts
| `-- b
| `-- index.js
`-- tsconfig.json
其中 a/index.js 的内容为:
import { b } from '../b'
let a: number = b
a/index.ts 中引入了 b/index.js,但是因为没有声明文件,编译系统会报错:
error TS7016: Could not find a declaration file for module '../b'. 'src/b/index.js' implicitly has an 'any' type.
我们可以通过添加一个 d.ts 文件来解决这个问题。如果 b/index.js 的内容为:
export let b = 1
我们可以在 b 目录下创建名称为 index.d.ts 的类型声明文件:
export let b: number
编译器在遇到 Javascript 模块时,会尝试在同一目录下寻找与 js 文件名称相同的 d.ts 文件,如果有就自动作为类型声明文件。
除了声明导出变量,也可以声明导出函数、对象接口和类。示例:
export function c(arg: string): void
export let d: { value: number }
export interface E {
value: number
}
export class F {
value: number
}
d 对于 ESM 的默认导出,需要使用declare关键词先声明后导出。示例:
declare let b: number
export default b
注意:在类型声明文件中使用默认导出需要配置esModuleInterop: true 。
类型声明文件可以导入其它模块的类型,使用方法和模块文件相同。示例:
import { Dayjs } from 'dayjs'
declare let b: Dayjs
export default b
node_modules 模块
<<<<<<< HEAD 有的第三方 Javascript 包已经提供了类型声明文件,那么直接引用就可以。有的包没有类型声明文件,我们可以尝试在 NPM 库中搜索已@types 为前缀的包,如果有安装即可,编译器会自动 node_modules/@types 中搜索。
比如我们如果需要使用 lodash,除了安装 lodash 外,还需要安装@types/lodash。
如果第三方包没有在包被提供类型声明文件,也没有提供相应的@types 包,那么需要我们自己编写类型声明文件。
编写的方法是创建一个全局类型声明文件,使用declare module语法。示例:
有的第三方 Javascript 包已经提供了类型声明文件,那么直接引用就可以。有的包没有类型声明文件,我们可以尝试在 NPM 库中搜索已@types 为前缀的包,如果有安装即可,编译器会自动 node_modules/@types 中搜索。如果需要查询模块的 API,可以到 node_modules 响应的声明文件中查询。比如我们如果需要使用 lodash,除了安装 lodash 外,还需要安装@types/lodash。
如果第三方包没有在包被提供类型声明文件,也没有提供相应的@types 包,那么需要我们自己编写类型声明文件。编写的方法是创建一个全局类型声明文件,使用declare module语法。示例:
62307dd48b43295668eb3cbe6e0fa4c9c5d7a190
declare module 'name' {
export let a: number
}
其中 name 为模块的名称,大括号中的语法与模块声明文件中的语法相同。
如果不想具体声明模块的内容,可以仅仅显式声明模块为any类型,方法是使用declare module语法但不编写大括号内的内容即可。示例:
declare module 'name'
node 原生模块
node 原生模块也需要安装类型声明文件:@types/node。
其它类型文件
在一些前端框架(比如 Vue)项目中,我们会引入非 Typescript 也非 Javascript 的文件,通常由 Webpack 或其它打包工具帮我们将这类文件转译成了 ts 或 js 文件,但是我们在 ts 文件内引入时,仍然是引入了比如.vue文件,而 Typescript 编译器是无法识别的。此时需要我们告诉编译器,这些文件是有效的模块,且声明模块的成员类型。
比如,在使用了 Vue-Cli 创建的的 Vue 的项目中通常可以看到shims-vue.d.ts文件,其内容为:
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
这个文件就声明了.vue文件导出的是一个模块,其模块的类型是 Vue 包中声明的DefineComponent。编译器接着会到 Vue 包中寻找相应的类型声明,这样我们就可以导入.vue文件并正常的使用定义好的组件。
这种声明的文件名没有要求
全局变量
我们可以在全局类型声明文件中声明全局变量。
示例:
let a: number = b
我们在一个文件中使用了变量b,但是 c 既没有在这个文件中定义,也没有从外部导入,那么编译器认为这是一个没有定义的变量进而报错。
我们可以在工作目录的任意位置创建一个任意名称的全局类型声明文件。比如在根目录创建 global.d.ts:
declare var b: number
编译器就会知道b是一个全局变量。
也可以声明函数、对象接口和类。示例:
declare function c(arg: string): void
declare var d: { value: number }
declare interface E {
value: number
}
declare class F {
value: number
}
在模块声明文件中也可以声明全局类型,需要使用declare global语法(node 中)。示例:
declare global {
interface Object {
toJSON(): string
}
}
export {}
扩展
有时我们会对第三方包的模块注入新的成员,但是由于注入的属性并不在原本的包的类型声明中,所以编译器无法识别。
我们可以通过声明已存在的模块的方式为模块内接口添加额外的属性。
比如,原本 M 的模块有这样一个接口:
interface A {
name: string
}
现在我们在 js 文件中对它进行了扩展,注入了value属性。为了在 ts 文件中也能使用这个被注入的属性,我们可以在类型声明文件中额外声明这个模块的接口:
declare module M {
interface A {
value: string
}
}
因为 Typescript 中的接口是可以在多处多次声明的,这里额外声明的属性会被合并到原本的 A 接口中。这样,编译器就可以识别 A 实例中的 value 属性。
这个方法同样适用于对 Javascript 原生对象的扩展,比如:
interface Object {
toJSON(): string
}
然后我们在其它地方通过对 Object 原型注入toJSON方法的实现后,就可以直接使用这个方法了。
如果我们需要在浏览器环境的全局变量window中注入自定义成员,但是编译器无法通过(因为原本的window变量的类型Window中没有这个成员),此时我们可以通过扩展Window来添加这个成员。示例:
interface Window {
log(...args: string[]): void
}