首页 前端知识 TypeScript(十二)模块

TypeScript(十二)模块

2024-05-05 12:05:58 前端知识 前端哥 940 663 我要收藏

目录

引言

d.ts声明文件

declare关键字

全局声明

全局声明方式

全局声明一般用作

函数声明

在.ts中使用declare

外部模块(文件模块)

模块关键字module

声明模块

模块声明方式

模块通配符

模块导出

模块嵌套

模块的作用域

模块别名

内部模块(命名空间)

命名空间 OR 模块?

global关键字

总结

参考文章


引言

本文收录于TypeScript知识总结系列文章,欢迎指正! 

将体量大的程序拆分成多个小的,功能独立的模块是开发中不可或缺的一环,开发复杂程序的核心之一就是让其变得不复杂。模块化开发可以提高代码的可维护性、可重用性、可扩展性和可测试性,从而提高了开发效率和代码质量,TypeScript沿用了JS的模块概念,在之前文章中我介绍过Node环境下的两种类型兼容,顺带提了一下目前常用的模块导入导出方式:Commonjs和ES Module,这两种方式在TS中被称为是外部模块,除此之外TS还包含了内部模块和全局模块,本文将逐一介绍

d.ts声明文件

在编译后的JS文件的同一级常能看到.d.ts后缀的声明文件,其作用是描述代码中已经存在的类型信息或为其提供类型声明。举个例子,使用第三方库时可能会找不到对应的类型信息,于是TS提供了声明文件这个概念,它使开发者拥有对库进行描述的能力,达到静态类型提示或者TS检查的目的,声明文件编译后不会生成任何js代码。

一般声明文件内部是不包含可执行语句的,只有类型或者变量的声明。在开发时通常会在项目根目录中新建一个像global.d.ts(名字自取)的文件用于描述全局的类型,变量,函数,类等等

declare关键字

declare是描述TS文件之外信息的一种机制,它的作用是告诉TS某个类型或变量已经存在,我们可以使用它声明全局变量、函数、类、接口、类型别名、类的属性或方法以及后面会介绍的模块与命名空间

全局声明

通常在global.d.ts文件中使用declare关键字进行全局声明,以便目录下所有文件都能直接访问

全局声明方式

  • declare var 名称: 变量
  • declare const / let 名称: ES6变量
  • declare function 名称: 方法
  • declare class 名称: 类
  • declare enum 名称: 枚举
  • declare module 名称: 模块
  • declare namespace 名称: 命名空间
  • declare interface 名称: 接口
  • declare type 名称: 类型别名

全局声明一般用作

  • 描述全局变量或类型
  • 描述第三方库的类型
  • 描述全局模块

举个例子,在项目根目录新建global.d.ts用于变量类型的全局声明,接着修改tsconfig中配置include为["global.d.ts", "src"],在项目任意目录新建index.ts

// global.d.ts
declare interface IAnimal {
    name: string
    age?: number
}

declare let animal: IAnimal
// src/index.ts
animal = {
    name: "阿黄"
}

可以看到index.ts文件中animal的类型是global.d.ts中声明的变量,其二者产生了关联

 

tips:声明文件(d.ts)中的所有类型(类型别名和接口除外)及变量都要使用declare定义,或者使用export将其导出,否则会抛出以下错误:.d.ts 文件中的顶级声明必须以 "declare" 或 "export" 修饰符开头。

此外,使用类型别名和接口定义的类型可以不需要声明,直接在全局访问

declare type str = string
// 相当于type str = string

函数声明

参照上面的定义方式,我们可以在声明文件中声明一个函数,然后再使用前对函数进行实现或重载

// global.d.ts
declare function add(a: number, b: number): number;

// src/index.ts
function add(a: number, b: number) {
    return a + b
}
console.log(add(1, 2));// 3

// src/main.ts
function add(a: number, b: number, c: string) {
    return a + b + c
}
console.log(add(1, 2, "3"));// 33

对函数声明进行重载

在.ts中使用declare

我们在介绍属性装饰器的时候曾用到了declare关键字,当时并没有具体说明使用它的原因,这里咱们详细分析一下,首先贴出一段类似的代码

class Animal {
    name?: string;
}

在ES2022及以后类中定义的属性会在编译后保留在类中,就像

class Animal {
    name;
}

而在.ts文件中使用declare只会被当成是类型或者变量的定义,最后编译在声明文件.d.ts中,不会编译在.js文件中,就像下面这个类

// index.ts
declare class Animal {
    name?: string;
}

// index.js
// 空文件

// index.d.ts
declare class Animal {
    name?: string;
}

通过declare这个特点,我们可以在类中属性或者方法定义时使用declare关键字将其指定为声明类型的变量,不会出现在.js中,有效的解决之前的问题

// index.ts
class Animal {
   declare name?: string;
}

// index.js
class Animal {}

外部模块(文件模块)

在TS中模块既可以以单个文件的形式存在,这与JS相同,通过export和import两个关键字进行导出导入,对应的介绍可以参照这篇文章的ESM部分,也可以使用module关键字定义模块。

与JS稍有不同,TS中包含了接口和类型别名,我们同样可以通过export type 类型名 导出对应类型别名,如

// src/main.ts
export type IAnimal = {
    name: string
    color?: string
}

// src/index.ts
import { IAnimal } from './main'
const animal: IAnimal = {
    name: "阿黄",
};

tips:在一个.d.ts文件中使用export关键字会使这个文件成为一个模块(这点很重要,一个声明文件(d.ts)不是全局声明文件(只使用declare声明类型)就得是文件模块(使用export等关键字导出)),比如我们把上面的global文件和index改成下面代码

// global.d.ts
type IAnimal = {
    name: string
    color?: string
}
export {}

// index.ts
const animal: IAnimal = {
    name: "阿黄",
};

此时直接使用全局的IAnimal就会抛错

必须使用export将IAnimal导出并使用import导入该模块中的类型

模块关键字module

声明模块

除了上面的使用方式外,我们可以使用module关键字在一个文件中定义多个模块,如

// global.d.ts
declare module 'global_type' {
    export type IAnimal = {
        name: string
    }
    export type ICat = {
        name: string
    }
}
declare module 'global_type1' {
    export type IDog = {
        name: string
    }
}

// index.ts
import type { IAnimal, ICat } from "global_type"
import type { IDog } from 'global_type1'
const animal: IAnimal = {
    name: "阿黄",
};
const dog: IDog = animal
const cat: ICat = animal

每一个使用module定义的内容是一个模块

模块声明方式

TS支持CommonJS和ESM两种模块系统,使得声明模块有两种写法,分别是使用字符串和变量名

CommonJS的写法遵循匹配文件的相对或绝对路径,通常模块名作为字符串字面量,该方法不支持导出模块,只允许使用declare定义全局模块,并且使用时需要使用import导入

// global.d.ts 
declare module "global_type" {
    export type IAnimal = {
        name: string
    }
}

// src/index.ts
import * as global_type from "global_type"
const myObject: global_type.IAnimal = {}

ESM的写法和定义变量一样,使用变量名匹配标识符进行模块的导入,这种方式与定义命名空间(namespace)的效果一样,使用ESM定义的全局模块可以直接使用,不需要导入

// global.d.ts 
declare module global_type {
    export type IAnimal = {
        name?: string
    }
}

// src/index.ts
const myObject: global_type.IAnimal = {}

模块通配符

我们在webpack或者vite等工具中可能会看到类似下面的代码,这种写法是CommonJS模块系统独有的

declare module '*.type' {
    export type IDog = {
        name: string
    }
}

这段代码中使用了*.type通配符,匹配了所有.type结尾的模块,导入.type类型的文件就会有IDog这个类型

import type { IDog } from 'global_type.type'
const animal: IDog = {
    name: "阿黄",
};
const dog: IDog = animal

模块导出

使用module定义的模块遵循全局声明,同样可以使用export导出并在其他文件使用,这种方式是ESM模块系统独有的

// global.d.ts 
export module global_type {
    export type IAnimal = {
        name: string
    }
    export class Animal implements IAnimal {
        name: string
    }
}

// index.ts
import { global_type } from '../global'
const myObject: global_type.IAnimal = new global_type.Animal();

模块嵌套

模块嵌套可以应对更复杂的结构,避免命名冲突,全局污染,在模块中写模块不需要declare和export关键字,模块默认自带导出

// global.d.ts 
declare module global_type {
    export module IAnimalModule {
        export let animal: IAnimal
        export type IAnimal = {
            name: string
        }
    }
    module IDogModule {
        let dog: IDog
        type IDog = {
            name?: string
        }
    }
}
// src/index.ts
let animal: global_type.IAnimalModule.IAnimal = global_type.IAnimalModule.animal
let dog: global_type.IDogModule.IDog = global_type.IDogModule.dog

模块的作用域

在模块嵌套时,我们可以把 module { } 或者 namespace { } 的大括号中的作用域称为模块的作用域,作用域中的模块类型可以访问,此时如果在模块中使用 export {} 导出空对象的话,当前模块就会被视为文件模块(这和我们上面说到的全局文件转模块文件的tips类似),需要使用export导出局部类型、变量、模块,否则会默认不做导出操作,成为一个私有的模块:

// global.d.ts 
declare module global_type {
    module IAnimalModule { // 局部模块,只能在global_type中使用
        let animal: IAnimal
        type IAnimal = {
            name?: string
        }
    }
    export { }
}

// src/index.ts
let animal: global_type.IAnimalModule.IAnimal// “global_type”没有已导出的成员“IAnimalModule”

此时需要将模块中的模块手动导出,或加入到导出对象中

export module IAnimalModule {
    let animal: IAnimal
    type IAnimal = {
        name?: string
    }
}
// 或者
export { IAnimalModule }

模块别名

模块的别名是一种用来简化其访问的方式,可以使用import关键字来定义一个别名,然后用这个别名来代替原来的名称,比如

// global.d.ts 
declare module global_type {
    export type IAnimal = {
        name?: string
    }
    export class Animal implements IAnimal { }
}

// src/index.ts
import Ani = global_type.Animal
import IAni = global_type.IAnimal
const ami: IAni = new Ani()

内部模块(命名空间)

因为1.5版本前的命名空间(namespace)是TS提出的模块理念,而上面说到的模块是JS中的ES标准,TypeScript对二者做了区分,所以它被称为内部模块。它同样是模块化机制的一员,它的作用是将全局的变量,函数,类等等封装在一个空间内,防止命名污染,冲突。官方比较推荐使用namespace来代替module的ESM模块系统写法,所以在使用模块的变量写法时,TS会将其转换成namespace

 

还记得上面说的ESM模块系统的导出方式吗?我们把代码中的module关键字换成namespace,就大功告成了,命名空间拥有模块的变量写法的特性。

namespace global_type {
    export type IAnimal = {
        name: string
    }
    export class Animal implements IAnimal {
        name: string
    }
}
const myObject: global_type.IAnimal = new global_type.Animal();

我们把代码放到一个文件中解析一下编译后的JS文件

var global_type;
(function (global_type) {
    class Animal {
        name;
    }
    global_type.Animal = Animal;
})(global_type || (global_type = {}));
const myObject = new global_type.Animal();

可以看到,代码中使用iife产生了一个私有的作用域并且定义了一个空对象,将命名空间导出的变量放至对象中。

思考一个问题,一个命名空间必须通过一处代码块定义吗?

答案是否定的,类似函数重载,命名空间的定义允许声明合并,将同名的命名空间对象进行合并(后面的文章会说到,留个悬念)

命名空间 OR 模块?

模块适用于需要动态加载、封装和复用代码的场景,比如Node.js应用、Web应用、npm包等。模块可以利用模块加载器(如CommonJS/Require.js)或支持ES模块的运行时来管理依赖和导入导出。模块是ES6标准的一部分,是现代代码的推荐组织方式。

命名空间适用于需要在全局范围内定义变量、函数、类、接口等的场景,比如Web应用中使用<script>标签引入所有依赖的HTML页面。命名空间可以避免全局变量的命名冲突,但也会增加组件依赖的难度,尤其是在大型应用中。

global关键字

在TS中declare global关键字用于向全局作用域中添加类型或变量的声明

以我的理解,global的使用方式应该和module以及namespace类似,照葫芦画瓢,使用global { ... }作为一个代码块表示全局的作用域,在里面定义的类型以及变量应该是能够在任意地方取到的

// global.d.ts 
declare global {
    type IDog = {
        name: string
    }
    let animal: IDog
}

// src/index.ts
animal = {
    name: "阿黄"
}

然而事情并没有这么简单,它会提示:全局范围的扩大仅可直接嵌套在外部模块中或环境模块声明中。

在官方给出的d.ts模板中,代码最后一行做了一个导出的操作

这是为何?

实际上这是为了解决TS编译器的一个问题,上面我们说到,如果声明文件d.ts没有使用export导出或者没有使用declare定义类型、变量,那么编译器就会报错,而使用declare global和使用declare module、declare namespace、declare type、declare interface都不一样,这些类型或者模块的定义就是导出声明,不需要进行额外的export。所以在定义(说是拓展比较贴切)全局的global时会在代码底部导出空对象,来声明这个文件是有导出的

上面我们提到:使用export关键字会使声明文件成为一个模块;如果使用了declare global和export { },那么我们的全局变量和类型就不需要使用declare声明,可以直接写在declare global代码块中

下面是一个完整的示例

// global.d.ts 
declare global {
    type IDog = {
        name: string
    }
    let animal: IDog
}
export { }

// src/index.ts
animal = {
    name: "阿黄"
}

总结

文章写到这里也就结束了,本文简述了TS中模块的使用,针对声明文件,declare关键字,文件模块,命名空间以及全局global关键字这几个方面进行了介绍,同时提出了自己的看法及遇到的问题,希望能对你有帮助。

感谢你看到最后,如果觉得文章还不错的话,还请点赞收藏关注支持一下博主,对文章内容有任何问题还望在评论区留言或私信,感谢!

参考文章

命名空间和模块 - TypeScript 中文手册

TypeScript: Documentation - Global .d.ts

TypeScript: Documentation - Modules

typescript已经有模块系统了,为什么还需要namespace? - 知乎

转载请注明出处或者链接地址:https://www.qianduange.cn//article/6945.html
标签
评论
发布的文章

String转Json的几种方式

2024-05-09 11:05:04

iOS ------ JSONModel源码

2024-05-09 11:05:02

java去除 json 中的 \n, \t, \r

2024-05-09 11:05:57

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!