TypeScript(十二)模塊
目錄
引言
本文收錄于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: Documentation - Global .d.ts

浙公網安備 33010602011771號