Skip to content

一. 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 导出和导入

typescript
// 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 中,可能会导致命名冲突。

使文件显式成为模块

如果一个文件没有任何导入或导出,但你希望它被视为模块处理,可以添加以下语句:

typescript
export {}

示例:非模块文件的影响

在一个脚本文件中,变量和类型会被声明在共享的全局作用域,将多个输入文件合并成一个输出文件,或者在 HTML 使用多个 <script> 标签加载这些文件

typescript
// a.ts
let globalVar = 42

// b.ts
let globalVar = 'Hello' // 与 a.ts 中的变量冲突

// 引入到同一个 HTML 页面或合并编译时会报错

修复方式:显式声明为模块

typescript
// a.ts
export {}
let globalVar = 42

// b.ts
export {}
let globalVar = 'Hello'

实际应用场景

在项目中,将所有文件都视为模块是最佳实践,即便文件不导出任何内容,也应使用 export {} 显式声明模块化。

内置类型导入(Inline type imports)

引言

随着 TypeScript 4.5 的发布,新增了对内置类型导入的支持,这使得类型的引入变得更加灵活和高效。

为什么需要内置类型导入?

  • 在开发过程中,类型只在编译阶段起作用,不会出现在最终的运行时代码中。
  • 编译器需要明确哪些导入是类型,从而能够在编译后安全移除这些无用的代码,优化打包大小。

传统导入的缺陷

typescript
// 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
    typescript
    import { 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 编译器,比如 Babelswc 或者 esbuild 知道什么样的导入可以被安全移除

总结:

  • 这些类型在经过我们代码的编译阶段,全部会被删除掉,因为类型只是用来在开发阶段进行类型检测的,打包之后的生产环境中不需要类型检测
  • 对于编译器来说,对于这些导入的类型代码,如果通过语法来识别,需要先识别语法再进行删除,对编译器来说会增加工作量
  • 而如果在导入的同时明确指定了导入的类型,编译器之后在转换编译过程中,就可以直接删掉
  • 编译 ts 代码方式:tscbabel、...

二. 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'
      }
      typescript
      import { price, date } from './format'
      
      price.format('10')
      console.log(price.name) // price
      console.log(date.name) // date

三. 内置声明文件的使用


01. 类型的查找

  • 之前我们所有的 TS 中的类型,几乎都是我们自己编写的,但是我们也有用到一些其他的类型:

    typescript
    const 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声明文件

    • 包括比如 FunctionStringMathDate 等内置类型
    • 也包括运行环境中的 DOM API,比如 WindowDocument
  • TS 使用模式命名这些声明文件 lib.[something].d.ts

    image-20221019143526044
  • 内置类型声明通常在我们安装 TS 的环境中会带有的

03. 内置声明的环境

  • 我们可以通过 targetlib 来决定哪些内置类型声明是可以使用的:

    • 例如,startsWith 字符串方法只能从称为 ECMAScript 6JS 版本开始使用
  • 我们可以通过 target编译选项来配置TS 通过 lib 根据您的 target 设置更改默认包含的文件来帮助解决此问题

四. 第三方库声明的文件


01. 外部定义类型声明 – 第三方库

  • 外部类型声明通常是我们使用一些库(比如第三方库)时,需要的一些类型声明

  • 这些库通常有两种类型声明方式:

  • 方式一:在自己库中进行类型声明(编写 .d.ts 文件),比如 axios

  • 方式二:通过社区的一个公有库 DefinitelyTyped 存放类型声明文件

总结:

  1. 包本身包含声明文件,如 axios
  2. 如果没有包含,但在官方 DefineTypes 库中包含声明文件,安装对应的声明文件即可
    • 如:npm i @types/react -D
  3. 如果包本身没有声明,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)
      }
      typescript
      let _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 模块默认不能使用的情况,可以自己来声明这个模块:

    typescript
    declare 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 中配置了相关资源的处理规则

      typescript
      const 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 文件的目录

      typescript
      tsc
    • 调用 tsc 命令并且没有其他输入文件参数,可以使用 --project(或者只是 -p)的命令行选项来指定包含了 tsconfig.json 的目录

    • 当命令行中指定了输入文件参数, tsconfig.json 文件会被忽略

      typescript
      tsc xxx.js
  • webpack 中使用 ts-loader 进行打包时,也会自动读取 tsconfig 文件,根据配置编译 TS 代码

  • 实际开发中,也不会手动去编译文件,而是用 webpack 这类工具及相关的 loader,帮助我们进行编译打包

  • tsconfig.json 文件包括哪些选项呢?

    • tsconfig.json 本身包括的选项非常非常多,我们不需要每一个都记住
    • 可以查看文档对于每个选项的解释:https://www.typescriptlang.org/tsconfig
    • 当我们开发项目的时候,选择 TS 模板时,tsconfig 文件默认都会帮助我们配置好的
  • 接下来我们学习一下哪些重要的、常见的选项

03. tsconfig.json 顶层选项

image-20221020162610611image-20221020162625555

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"]
    }

Released under the MIT License.