一. TypeScript 模块使用
认识 TS 模块化
引言
JavaScript 在模块化方面经历了较长的历史演变,从早期的全局变量到 CommonJS、AMD 等模块化规范的兴起,再到现代的 ES Module(也就是我们熟悉的 import/export 语法)。
TypeScript 自 2012 年开始跟进支持这些模块化规范。
模块化的核心目标
- 解决代码组织问题,避免全局变量冲突。
- 提供更清晰的依赖管理,方便代码复用和维护。
- 提升编译器和开发工具的静态分析能力。
现代 TypeScript 的模块化支持
- TypeScript 的主要模块化方案基于 ES Module。
- ES Module 自 2015 年被纳入 ECMAScript 标准,到 2020 年基本被现代浏览器和 JavaScript 运行环境全面支持。
- TypeScript 同时也兼容其他历史规范(如 CommonJS),以支持在 Node.js 等环境中的广泛应用。
示例:使用 ES Module 导出和导入
// mathUtils.ts
export function add(a: number, b: number): number {
return a + b
}
export function subtract(a: number, b: number): number {
return a - b
}
// app.ts
import { add, subtract } from './mathUtils'
console.log(add(5, 3)) // 输出: 8
console.log(subtract(5, 3)) // 输出: 2
在前面我们已经学习过各种各样模块化方案以及对应的细节,这里我们主要学习 TS 中一些比较特别的细节。
非模块(Non-modules)
背景
在 JavaScript 中,并非所有文件都是模块。TypeScript 对模块的定义也非常明确。
什么是非模块?
- 一个没有使用 import 或 export 的文件被视为脚本,而不是模块。
- 在脚本文件中,变量和类型会共享全局作用域。
- 如果多个文件被合并或同时加载到 HTML 中,可能会导致命名冲突。
使文件显式成为模块
如果一个文件没有任何导入或导出,但你希望它被视为模块处理,可以添加以下语句:
export {}
示例:非模块文件的影响
在一个脚本文件中,变量和类型会被声明在共享的全局作用域,将多个输入文件合并成一个输出文件,或者在
HTML
使用多个<script>
标签加载这些文件
// a.ts
let globalVar = 42
// b.ts
let globalVar = 'Hello' // 与 a.ts 中的变量冲突
// 引入到同一个 HTML 页面或合并编译时会报错
修复方式:显式声明为模块
// a.ts
export {}
let globalVar = 42
// b.ts
export {}
let globalVar = 'Hello'
实际应用场景
在项目中,将所有文件都视为模块是最佳实践,即便文件不导出任何内容,也应使用 export {} 显式声明模块化。
内置类型导入(Inline type imports)
引言
随着 TypeScript 4.5 的发布,新增了对内置类型导入的支持,这使得类型的引入变得更加灵活和高效。
为什么需要内置类型导入?
- 在开发过程中,类型只在编译阶段起作用,不会出现在最终的运行时代码中。
- 编译器需要明确哪些导入是类型,从而能够在编译后安全移除这些无用的代码,优化打包大小。
传统导入的缺陷
// type.ts
export interface IUser {
name: string
age: string
}
export type ID = number | string
// app.ts
import { IUser, ID } from './types'
TS4.5
也允许单独的导入,你需要使用type
前缀 ,表明被导入的是一个类型:typescript// type.ts export interface IPerson { name: string age: number } export type IDType = number | string
typescriptimport { sum } from './test1' // 导入的是类型时,推荐在类型前面加上type关键字 import { type IDType, type IPerson } from './type' // 也可以使用下面的写法,作用等价的 // import type { IDType, IPerson } from './type' console.log(sum(10, 20)) const id: IDType = 100 const person: IPerson = { name: 'later', age: 10 }
这样可以让一个非
TS
编译器,比如Babel
、swc
或者esbuild
知道什么样的导入可以被安全移除
总结:
- 这些类型在经过我们代码的编译阶段,全部会被删除掉,因为类型只是用来在开发阶段进行类型检测的,打包之后的生产环境中不需要类型检测
- 对于编译器来说,对于这些导入的类型代码,如果通过语法来识别,需要先识别语法再进行删除,对编译器来说会增加工作量
- 而如果在导入的同时,明确指定了导入的是类型,编译器之后在转换编译过程中,就可以直接删掉
- 编译
ts
代码方式:tsc
、babel
、...
二. TypeScript 命名空间
01. 命名空间 namespace
TS
有它自己的模块格式,名为namespaces
,它在ES
模块标准之前出现命名空间在
TS
早期时,称之为内部模块,目的是将一个模块内部再进行作用域的划分,防止一些命名冲突的问题虽然命名空间没有被废弃,但是由于
ES
模块已经拥有了命名空间的大部分特性因此更推荐使用
ES
模块,这样才能与JS
的发展方向保持一致typescript// format.ts export namespace price { export function format(price: string) { return `¥${price}` } export const name = 'price' } export namespace date { export function date(dateString: string) { return '2020-10-22' } export const name = 'date' }
typescriptimport { price, date } from './format' price.format('10') console.log(price.name) // price console.log(date.name) // date
三. 内置声明文件的使用
01. 类型的查找
之前我们所有的
TS
中的类型,几乎都是我们自己编写的,但是我们也有用到一些其他的类型:typescriptconst imageEl = document.getElementById('image') as HTMLImageElement
大家是否会奇怪,我们的
HTMLImageElement
类型来自哪里呢?甚至是document
为什么可以有getElementById
的方法呢?- 其实这里就涉及到
typescript
对类型的管理和查找规则了
- 其实这里就涉及到
这里先给大家介绍另外的一种
TS
文件:.d.ts
文件- 我们之前编写的
TS
文件都是.ts
文件,这些文件最终会输出.js
文件,也是我们通常编写代码的地方 - 还有另外一种文件
.d.ts
文件,它是用来做类型的声明(declare
),称之为类型声明(Type Declaration
)或者类型定义(Type Definition
)文件 - 它仅仅用来做类型检测,告知
TS
我们有哪些类型
- 我们之前编写的
那么
TS
会在哪里查找我们的类型声明呢?- 内置类型声明
- 外部定义类型声明
- 自己定义类型声明
02. 内置类型声明
内置类型声明是
TS
自带的、帮助我们内置了JS
运行时的一些标准化API
的声明文件- 包括比如
Function
、String
、Math
、Date
等内置类型 - 也包括运行环境中的
DOM API
,比如Window
、Document
等
- 包括比如
TS
使用模式命名这些声明文件lib.[something].d.ts
内置类型声明通常在我们安装
TS
的环境中会带有的
03. 内置声明的环境
我们可以通过
target
和lib
来决定哪些内置类型声明是可以使用的:- 例如,
startsWith
字符串方法只能从称为ECMAScript 6
的JS
版本开始使用
- 例如,
我们可以通过
target
的编译选项来配置:TS
通过lib
根据您的target
设置更改默认包含的文件来帮助解决此问题
四. 第三方库声明的文件
01. 外部定义类型声明 – 第三方库
外部类型声明通常是我们使用一些库(比如第三方库)时,需要的一些类型声明
这些库通常有两种类型声明方式:
方式一:在自己库中进行类型声明(编写
.d.ts
文件),比如axios
方式二:通过社区的一个公有库
DefinitelyTyped
存放类型声明文件该库的
GitHub
地址:https://github.com/DefinitelyTyped/DefinitelyTyped/该库查找声明安装方式的地址:https://www.typescriptlang.org/dt/search?search=
比如我们安装
react
的类型声明:npm i @types/react --save-dev
总结:
- 包本身包含声明文件,如
axios
- 如果没有包含,但在官方
DefineTypes
库中包含声明文件,安装对应的声明文件即可
- 如:
npm i @types/react -D
- 如果包本身没有声明,
DefineTypes
库也没有,只能手写自定义声明文件
五. 编写自定义声明文件
01. 外部定义类型声明 – 自定义声明
什么情况下需要自己来定义声明文件呢?
情况一:我们使用的第三方库是一个纯
JS
库,没有对应的声明文件;比如lodash
情况二:我们给自己的代码中声明一些类型,方便在其他地方直接进行使用
typescript// later.d.ts declare let _name: string declare let _age: number declare let _height: number declare function foo(): void declare function bar(arg: number): void declare class Person { name: string age: number constructor(name: string, age: number) }
typescriptlet _name = 'later' let _age = 18 let _height = 1.8 function foo() {} function bar(arg: number) {} const p = new Person('later', 18) console.log(p.name, p.age)
02. declare 声明模块
我们也可以声明模块,比如
lodash
模块默认不能使用的情况,可以自己来声明这个模块:typescriptdeclare module 'lodash' { export function join(arg: any[]): any }
声明模块的语法:
typescript// declare module '模块名' {} declare module 'moduleName' {}
- 在声明模块的内部,可以通过
export
导出对应库的类、函数等
- 在声明模块的内部,可以通过
03. declare 声明文件
在某些情况下,我们也可以声明文件:
比如在开发
vue
的过程中,默认是不识别我们的.vue
文件的,那么我们就需要对其进行文件的声明比如在开发中我们使用了
jpg
这类图片文件,默认TS
也是不支持的,也需要对其进行声明typescript// 声明文件模块,声明之后ts就会允许我们引入 declare module '*.png' // 文件类型一般不需要在后面加{},都是直接声明该文件即可 declare module '*.jpg' declare module '*.jpeg' declare module '*.svg' declare module '*.vue'
typescript// 可以引入对应的.png、.vue文件了 import KobeImage from './img/kobe02.png' import App from './vue/App.vue'
虽然引入,但是加载对应的图片,还需要
webpack
中配置了相关资源的处理规则typescriptconst path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.css$/i, use: ['style-loader', 'css-loader'], }, + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', + }, ], }, };
04. declare 命名空间
比如我们在
index.html
中直接引入了jQuery
:我们可以进行命名空间的声明:
typescript// 为什么不用模块声明,而是命名空间的声明? // 模块声明需要引入模块,而$应该是直接用,不需要每次都引入,所以使用命名空间来声明 declare namspace $ { function ajax(settings: any): void }
在
main.ts
中就可以使用了:typescript$.ajax({ url: 'http://codercba.com:8000/home/multidata', success: function (res: any) { console.log(res) } })
六. tsconfig 配置文件解析
01. 认识 tsconfig.json 文件
- 什么是
tsconfig.json
文件呢?(官方的解释)- 当目录中出现了
tsconfig.json
文件,则说明该目录是TS
项目的根目录 tsconfig.json
文件指定了编译项目所需的根目录下的文件以及编译选项
- 当目录中出现了
tsconfig.json
文件有两个作用:- 作用一(主要的作用):让
TS Compiler(tsc)
在编译的时候,知道如何去编译TS
代码和进行类型检测- 比如是否允许不明确的
this
选项,是否允许隐式的any
类型 - 将
TS
代码编译成什么版本的JS
代码
- 比如是否允许不明确的
- 作用二:让编辑器(如
VSCode
)可以按照正确的方式识别TS
代码- 对于哪些语法进行提示、类型错误检测等等
- 作用一(主要的作用):让
JS
项目可以使用jsconfig.json
文件,它的作用与tsconfig.json
基本相同,只是默认启用了一些JS
相关的编译选项- 在之前的
Vue
项目、React
项目中我们也有使用过
- 在之前的
02. tsconfig.json 配置
tsconfig.json
在编译时如何被使用呢?在调用
tsc
命令并且没有其它输入文件参数时,编译器将由当前目录开始向父级目录寻找包含tsconfig
文件的目录typescripttsc
调用
tsc
命令并且没有其他输入文件参数,可以使用--project
(或者只是-p
)的命令行选项来指定包含了tsconfig.json
的目录当命令行中指定了输入文件参数,
tsconfig.json
文件会被忽略typescripttsc xxx.js
webpack
中使用ts-loader
进行打包时,也会自动读取tsconfig
文件,根据配置编译TS
代码实际开发中,也不会手动去编译文件,而是用
webpack
这类工具及相关的loader
,帮助我们进行编译打包tsconfig.json
文件包括哪些选项呢?tsconfig.json
本身包括的选项非常非常多,我们不需要每一个都记住- 可以查看文档对于每个选项的解释:https://www.typescriptlang.org/tsconfig
- 当我们开发项目的时候,选择
TS
模板时,tsconfig
文件默认都会帮助我们配置好的
接下来我们学习一下哪些重要的、常见的选项
03. tsconfig.json 顶层选项
4. tsconfig.json 文件
tsconfig.json
是用于配置TS
编译时的配置选项:我们这里讲解几个比较常见的(这里用
vue cli
创建出来的项目默认配置):json{ "compilerOptions": { // esnext:和es版本保持一致,es更新到什么版本,就编译出最新的版本 // 那用编译到最新版本,浏览器支持吗?那为什么vue还用这个呢? // 因为vue不用ts来编译,而是用babel来转换的,babel根据.browserslistrc中配置的浏览器适配的规则来进行转换成最终代码的 "target": "esnext", // 生成的代码所使用的模块化 "module": "esnext", // 打开所有的严格模式检查(其他严格模式选项都打开,如果其他选项单独有设置会覆盖这里的) "strict": true, // 是否允许编写js代码 "allowJs": false, // 是否允许有模糊的any "noImplicitAny": false, // jsx的处理方式(保留原有的jsx格式,这里会用babel来转换) "jsx": "preserve", // 是否帮助导入一些需要的功能模块 "importHelpers": true, // 按照node的模块解析规则 // https://www.typescriptlang.org/docs/handbook/module-resolution.html#module-resolution-strategies "moduleResolution": "Node", // 跳过对整个库进行类似检测,而仅仅检测你用到的类型 "skipLibCheck": true, // 可以让 es module 和 commonjs 相互调用 "esModuleInterop": true, // 允许合成默认模块导出 // import * as react from 'react': false // import react from 'react': true "allowSyntheticDefaultImports": true, // 是否要生产sourcemap文件,sourcemap文件帮助我们查找代码报错的出处 "sourceMap": true, // 文件路径在解析时的基本url,'.'表示当前路径 "baseUrl": ".", // 指定types文件需要加载哪些(默认是都会进行加载的) // "types": [ // "webpack-env" // ], // 路径的映射设置,类似于webpack中的alias "paths": { "@/*": ["src/*"] }, // 指定我们需要使用到的库(也可以不配置,直接根据target来获取) "lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"] }, "include": [], "exclude": ["node_modules"] }